太长的 Haskell 函数的简化

Fab*_*184 3 haskell

我编写了一个函数来通过 API 查询货币汇率。它工作正常,但代码太长且难以阅读。我认为有人能够帮助我简化这个过程,特别是因为有许多重复的模式和运算符,例如重复使用

编辑:我没有意识到绑定任何东西pure是绝对没用的!

... <&> (=<<) (something >>= pure) ...
Run Code Online (Sandbox Code Playgroud)

我刚刚开始学习 Haskell,因此不知道可以在这里使用的许多聪明的运算符/函数/镜头。

顺便说一句,我知道存在 do 符号。

forex :: (String, String) -> IO (Maybe (Scientific, UnixTime))
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))
Run Code Online (Sandbox Code Playgroud)

收到的 JSON 看起来像这样

{"rates":{"EURUSD":{"rate":1.087583,"timestamp":1649600523}},"code":200}
Run Code Online (Sandbox Code Playgroud)

提前致谢。

Dan*_*ner 13

哇,那太长了。让我们一步一步来;最后,我们将得到以下代码片段,我发现它读起来更自然,但执行完全相同的计算:

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)
Run Code Online (Sandbox Code Playgroud)

让我们看看如何。

  1. 首先,我认为如果有策略性地选择换行符,则更容易查看和编辑。

    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))
    
    Run Code Online (Sandbox Code Playgroud)
  2. 单子定律之一是m >>= pure = m,所以让我们删除>>= pure所有地方。(第 4、7、8 和 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))
    
    Run Code Online (Sandbox Code Playgroud)
  3. 另一个单子定律是m >>= pure . f = fmap f m。让我们尽可能用这条定律来简化。(第 8 行和第 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))
    
    Run Code Online (Sandbox Code Playgroud)
  4. 的使用uncurry正在发生,因为我们没有对 进行模式匹配cp。让我们解决这个问题。(1、2 和 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))
    
    Run Code Online (Sandbox Code Playgroud)
  5. 我的心理类型检查器快疯了。让我们把这个计算分成三种不同的东西:一种在 中工作IO,一种在 中工作Maybe,一种是纯粹的。首先,让我们将其IO与其他所有内容分开。

    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))
    
    Run Code Online (Sandbox Code Playgroud)
  6. 现在让我们把各个部分分开Maybe

    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))
    
    Run Code Online (Sandbox Code Playgroud)
  7. 让我们把纯净的部分分开。函子定律之一是fmap f . fmap g = fmap (f . g),所以我们可以将这三个fmaps 合并到 中extractFirstTime。此时,(&)剩下的两个参数足够短,我们可以内联 的定义(&)。我还将使用名称(<$>)而不是fmap; 我觉得这样读起来比较清晰一些。

    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))
    
    Run Code Online (Sandbox Code Playgroud)
  8. Data.Map有一个名称map snd . toList,即elems。让我们使用模式匹配来挑选我们想要的元素,而不是使用headand 。!!(所有更改均在 中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)
    
    Run Code Online (Sandbox Code Playgroud)

可能还可以做一些额外的美化事情(我想到了添加类型签名,我有几个改变/改进代码行为的想法),但我认为到目前为止,您已经有了一些相当合理的阅读和理解内容。一路走来,使内容变得可读的一个副作用是消除了令人不安的重复代码片段,所以这是一个小小的好处;但如果它们仍然存在,那么尝试以额外步骤解决它们将是很自然的。