【问题标题】:What is a clean way of writing this function?编写此函数的简洁方法是什么?
【发布时间】:2021-03-04 03:41:58
【问题描述】:

对于我正在尝试解决的问题,我需要一些帮助。假设我有一个名为 Thing 的类型:

data Thing = ....

我想写一个函数,给定一个字符串,尝试将它与我的状态中的一些东西匹配并返回一个Thing

findFirstMatch :: String -> State (Maybe Thing)
    

问题是,要匹配那个字符串,它需要一个可能的字符串列表来匹配它。该列表由为我的状态定义的函数提供:

getPossibilities :: State String

现在,我需要调用第三个函数来接收原始字符串和其中一种可能性,并返回一个Maybe Thing

tryToMatch :: String -> String -> State (Maybe Thing)

我怎么写findFirstMatch?我想过这样做,但看起来不太干净,感觉可能已经实现了一些东西:

findFirstMatch :: String -> State (Maybe Thing)
findFirstMatch str = do
    xs <- getPossibilities
    firstNotNull (map (tryToMatch str) xs)

firstNotNull :: [State (Maybe Thing)] -> State (Maybe Thing)
firstNotNull [] = return Nothing
firstNotNull (x:xs) = do
    r <- x
    case r of
        Just _ -> return r
        Nothing -> firstNotNull xs

【问题讨论】:

  • 你和this person同班吗?
  • 大声笑,不,但我看到了相似之处。问题是我没有一个带有列表的单子,而是一个带有元素的单子列表。这就是为什么我不得不写那个丑陋的firstNotNull 东西

标签: haskell


【解决方案1】:

首先,如果你写firstNotNull 而不使用State,你可以清理很多。一个非常简单的传递是:

firstNotNull :: [Maybe Thing] -> Maybe Thing
firstNotNull [] = Nothing
firstNotNull (Just x:_) = Just x
firstNotNull Nothing:xs = firstNotNull xs

此外,您还可以使用Data.Maybe 中的一些函数进一步简化:

import Data.Maybe (catMaybes, listToMaybe)

firstNotNull :: [Maybe a] -> Maybe a
firstNotNull = listToMaybe . catMaybes

现在,让我们将注意力转向findFirstMatch,看看我们如何使用这个简化版的firstNotNull。第一个问题是:tryToMatch 真的需要住在State 吗?毕竟,它已经可以访问它所匹配的两个Strings。如果您可以将其类型更改为tryToMatch :: String -&gt; String -&gt; Maybe Thing,那么您基本上就可以了。

另一方面,如果tryToMatch 确实需要住在State 中,那么只需多做一点:我们需要将firstNotNull 传递给[Maybe Thing],但我们有一个@987654337 @。我们可以使用sequenceA 来解决这个问题,如下所示:

findFirstMatch :: String -> State (Maybe Thing)
findFirstMatch str = do
    xs <- getPossibilities
    fmap firstNotNull $ sequenceA (map (tryToMatch str) xs)

请注意,这仅在您的 State monad 足够懒惰时才有效。如果它太严格,最终会找到所有匹配项,做太多工作(并搞砸性能),然后返回第一个。

从这里,我们可以认识到sequenceAmap 的使用可以简化为对traverse 的一次调用,如下所示:

    fmap firstNotNull $ traverse (tryToMatch str) xs

这看起来干净多了!


当然,如果我们真的愿意,我们还可以走得更远。目前尚不清楚以下更改实际上是否使代码更清晰(相反,有一个强有力的论据认为它们会使代码更难阅读),但无论如何让我们玩得开心。

除了使用do,我们可以选择通过适当使用monadic bind 来使其成为单行:

findFirstMatch str = getPossibilities $ \xs -> (fmap firstNotNull $ sequenceA (map (tryToMatch str) xs))

内部 lambda 可以很好地 eta 简化为:

findFirstMatch str = getPossibilities >>= fmap firstNotNull . sequenceA . map (tryToMatch str)

这也可以减少:

findFirstMatch = (getPossibilities >>=) . ((fmap firstNotNull . sequenceA) .) . map . tryToMatch

虽然我们在处理它,但当我们可以内联它时,为什么还要定义 firstNotNull

findFirstMatch :: String -> State (Maybe Thing)
findFirstMatch = (getPossibilities >>=) . ((fmap (listToMaybe . catMaybes) . sequenceA) .) . map . tryToMatch

在那里,你的整个功能在一条凌乱的线上!

【讨论】:

  • 首先,非常感谢。没想到这么详细的回答。我会复习你所说的。
  • 是的,tryToMatch 需要住在State 因为其他一些要求。现在,您说如果我的 State monad 足够懒惰,这可以工作。我省略了细节,但我的 monad 实际上是应用于Except monad 的StateT monad 转换器,两者均由Control.Monad.Trans 提供。我不确定这是否足够懒惰,但由于firstNotNull 的新类型,它似乎需要接收所有已经计算的结果,然后返回第一个。
  • 如果你使用的是Control.Monad.Trans.State,那么你使用的是懒惰的(见here),所以你应该没问题。看起来它可以计算一切,但懒惰是很酷的。试试这个:evalState (head &lt;$&gt; sequence (repeat get)) 4。请注意,它创建了一个无限的get 操作列表,对它们进行排序,然后获取列表的头部。使用惰性StateT,这会产生4,但使用严格的,它会永远循环。
  • 那么这是一个不错的解决方案。顺便说一句,冷却一个班轮
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-03-03
  • 2012-05-08
  • 1970-01-01
  • 2022-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多