简化太长 Haskell 函数

Simplification of way too long Haskell function

我编写了一个函数来从 API 查询货币汇率。它工作正常,但代码太长且不可读。我以为有人能帮我简化这个,特别是因为有很多重复的模式和运算符,比如重复使用

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

... <&> (=<<) (something >>= pure) ...

我刚刚开始学习 Haskell,因此不知道有多少可以在这里使用的聪明 operators/functions/lenses。

顺便说一句,我知道存在 do-notation。

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))

收到的JSON是这样的

{"rates":{"EURUSD":{"rate":1.087583,"timestamp":1649600523}},"code":200}

提前致谢。

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

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)

让我们看看如何。

  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))
    
  2. monad 法则之一是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))
    
  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))
    
  4. uncurry 的使用正在发生,因为我们不在 cp 上 pattern-matching。让我们解决这个问题。 (第 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))
    
  5. 我的精神 type-checker 快要疯了。让我们把这个计算分成三种不同的东西:一种适用于 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))
    
  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))
    
  7. 然后我们把纯净的部分分开。函子定律之一是fmap f . fmap g = fmap (f . g),所以我们可以把三个fmap合并到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))
    
  8. Data.Map有个名字叫map snd . toList,即elems。我们不使用 head!!,而是使用模式匹配来挑选我们想要的元素。 (所有更改都在 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)
    

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