To start, I think it's easier to see and edit if there are strategically chosen line breaks.
forex cp =
(get ("https://www.freeforexapi.com/api/live?pairs=" ++ uncurry (++) cp)
<&> decode . flip (^.) responseBody
<&> (=<<) (parseMaybe (.: "rates") >>= pure)
:: IO (Maybe (Map Key (Map Key Scientific)))
)
<&> (=<<) (Data.Map.lookup (fromString (uncurry (++) cp)) >>= pure)
<&> (=<<) ((pure . toList) >>= pure)
<&> (=<<) (pure . map snd >>= pure)
<&> fmap (\y -> (head y, UnixTime ((CTime . fromRight 0 . floatingOrInteger) (y !! 1)) 0))
One of the monad laws is m >>= pure = m
, so let's delete >>= pure
everywhere. (One each on lines 4, 7, 8, and 9.)
forex cp =
(get ("https://www.freeforexapi.com/api/live?pairs=" ++ uncurry (++) cp)
<&> decode . flip (^.) responseBody
<&> (=<<) (parseMaybe (.: "rates"))
:: IO (Maybe (Map Key (Map Key Scientific)))
)
<&> (=<<) Data.Map.lookup (fromString (uncurry (++) cp))
<&> (=<<) (pure . toList)
<&> (=<<) (pure . map snd)
<&> fmap (\y -> (head y, UnixTime ((CTime . fromRight 0 . floatingOrInteger) (y !! 1)) 0))
Another monad law is m >>= pure . f = fmap f m
. Let's simplify with that law where possible. (One each on lines 8 and 9.)
forex cp =
(get ("https://www.freeforexapi.com/api/live?pairs=" ++ uncurry (++) cp)
<&> decode . flip (^.) responseBody
<&> (=<<) (parseMaybe (.: "rates"))
:: IO (Maybe (Map Key (Map Key Scientific)))
)
<&> (=<<) Data.Map.lookup (fromString (uncurry (++) cp))
<&> fmap toList
<&> fmap (map snd)
<&> fmap (\y -> (head y, UnixTime ((CTime . fromRight 0 . floatingOrInteger) (y !! 1)) 0))
The uses of uncurry
are happening because we're not pattern-matching on cp
. Let's fix that up. (Lines 1, 2, and 7.)
forex (c, p) =
(get ("https://www.freeforexapi.com/api/live?pairs=" ++ c ++ p)
<&> decode . flip (^.) responseBody
<&> (=<<) (parseMaybe (.: "rates"))
:: IO (Maybe (Map Key (Map Key Scientific)))
)
<&> (=<<) Data.Map.lookup (fromString (c ++ p))
<&> fmap toList
<&> fmap (map snd)
<&> fmap (\y -> (head y, UnixTime ((CTime . fromRight 0 . floatingOrInteger) (y !! 1)) 0))
My mental type-checker is going nuts. Let's split this calculation into three different kinds of things: one that works in IO
, one that works in Maybe
, and one that is pure. First let's split the IO
from everything else.
forex (c, p) = extractFirstTime c p
<$> get ("https://www.freeforexapi.com/api/live?pairs=" ++ c ++ p)
extractFirstTime c p response = response
& decode . flip (^.) responseBody
& (=<<) (parseMaybe (.: "rates"))
& (=<<) Data.Map.lookup (fromString (c ++ p))
& fmap toList
& fmap (map snd)
& fmap (\y -> (head y, UnixTime ((CTime . fromRight 0 . floatingOrInteger) (y !! 1)) 0))
Now let's split out the Maybe
parts.
forex (c, p) = extractFirstTime c p
<$> get ("https://www.freeforexapi.com/api/live?pairs=" ++ c ++ p)
extractFirstTime c p response = parseAndLookUp c p (response ^. responseBody)
& fmap toList
& fmap (map snd)
& fmap (\y -> (head y, UnixTime ((CTime . fromRight 0 . floatingOrInteger) (y !! 1)) 0))
parseAndLookUp c p body =
decode body >>=
parseMaybe (.: "rates") >>=
Data.Map.lookup (fromString (c ++ p))
And let's split out the pure parts. One of the functor laws is fmap f . fmap g = fmap (f . g)
, so we can merge the three fmap
s in extractFirstTime
. At that point, the two arguments to (&)
that remain are short enough that we can inline the definition of (&)
. I'll also use the name (<$>)
instead of fmap
; I think it reads a bit clearer.
forex (c, p) = extractFirstTime c p
<$> get ("https://www.freeforexapi.com/api/live?pairs=" ++ c ++ p)
extractFirstTime c p response = firstTime
<$> parseAndLookUp c p (response ^. responseBody)
parseAndLookUp c p body =
decode body >>=
parseMaybe (.: "rates") >>=
Data.Map.lookup (fromString (c ++ p))
firstTime m = m
& toList
& map snd
& (\y -> (head y, UnixTime ((CTime . fromRight 0 . floatingOrInteger) (y !! 1)) 0))
Data.Map
has a name for map snd . toList
, namely, elems
. Instead of using head
and !!
, let's use pattern matching to pick out the elements we want. (All changes are in firstTime
.)
forex (c, p) = extractFirstTime c p
<$> get ("https://www.freeforexapi.com/api/live?pairs=" ++ c ++ p)
extractFirstTime c p response = firstTime
<$> parseAndLookUp c p (response ^. responseBody)
parseAndLookUp c p body =
decode body >>=
parseMaybe (.: "rates") >>=
Data.Map.lookup (fromString (c ++ p))
firstTime = case Data.Map.elems m of
k:t:_ -> (k, UnixTime ((CTime . fromRight 0 . floatingOrInteger) t) 0)
可能还有其他可以做的美化工作(例如添加类型签名,我有几个想法可以改变/改进代码的行为),但我认为到这个时候,你已经有了一个相当合理的阅读和理解的东西。一路上,使事物可读性强,副作用是消除了你发现令人不安的重复代码片段,所以这是一个小奖励;但如果它们仍然存在,尝试将它们作为额外步骤来解决是非常自然的。
pat f = (=<<) (f >>= pure)
。然后,不必每次都编写它,只需使用pat
即可。 - David YoungMaybe
中。考虑合并它们,使所有操作都在aeson的单子中完成;例如,do { rates <- o .: "rates"; current <- rates .: fromString (c ++ p); rate <- current .: "rate"; timestamp <- current .: "timestamp"; pure (rate, UnixTime (CTime timestamp) 0) } :: Parser (Scientific, UnixTime)
可以让您省略很多与Maybe
、fmap
和Either
相关的繁琐操作;它还可以更可预测地处理格式不正确或格式出乎意料的JSON数据,而不像您的toList
、head
和(!!)
那样。 - Daniel Wagner