【问题标题】:Haskell: How to organize a group of functions that all take the same argumentsHaskell:如何组织一组都采用相同参数的函数
【发布时间】:2013-06-18 19:02:59
【问题描述】:

我正在编写一个程序,其中包含多个采用相同参数的函数。为简单起见,这是一个有些人为的示例:

buildPhotoFileName time word stamp = show word ++ "-" ++ show time ++ show stamp
buildAudioFileName time word = show word ++ "-" ++ show time ++ ".mp3"
buildDirectoryName time word = show word ++ "_" ++ show time

假设我正在循环访问来自 IO 的资源以在运行时获取 timeword 参数。在这个循环中,我需要加入上述函数的结果以进行进一步处理,所以我这样做:

let photo = buildPhotoFileName time word stamp
    audio = buildAudioFileName time word
    dir   = buildDirectoryName time word
in ....

这似乎违反了“不要重复自己”的原则。如果以后我发现我想将word 更改为采用word 的函数,我可能会在let 表达式的开头创建一个新绑定,如下所示:

let wrd   = processWord word
    photo = buildPhotoFileName time wrd stamp
    audio = buildAudioFileName time wrd
    dir   = buildDirectoryName time wrd
in ....

并且每次我将word 写入wrd 时都必须更改,如果我记得更改某些函数调用而不是其他函数调用,则会导致错误。

在 OOP 中,我会通过将上述函数放入一个类中来解决这个问题,该类的构造函数将 timeword 作为参数。实例化的对象本质上是三个函数,它们分别是 timeword。如果我想确保函数接收processWord word 而不是word 作为“参数”,我可以在构造函数中调用processWord

有什么更好的方法可以更适合函数式编程和 Haskell?

【问题讨论】:

    标签: haskell dry


    【解决方案1】:

    既然您说您已准备好为此创建一个 OO 包装类,我假设您愿意更改您的功能。以下是一个生成您想要的所有三个结果的元组的函数:

    buildFileNames time word stamp = 
      ( show word ++ "-" ++ show time ++ show stamp,
        show word ++ "-" ++ show time ++ ".mp3",
        show word ++ "_" ++ show time )
    

    你可以像这样使用它:

    let wrd   = processWord word
        (photo, audio, dir) = buildFileNames time wrd stamp
        in ....
    

    如果您不需要任何结果,您可以像这样跳过它们:

    let wrd   = processWord word
        (_, audio, _) = buildFileNames time wrd stamp
        in ....
    

    值得注意的是,您不必担心 Haskell 将资源浪费在计算您不使用的值上,因为它是惰性的。

    【讨论】:

    • 非常好。我以前从未见过这个技巧,尽管事后看来很明显。一个小提示:三个结果字符串共享部分,所以我认为您可以插入一些 let -s 来明确共享这些部分。
    • @AndrásKovács 不需要,因为编译器正在执行common subexpression elimination
    【解决方案2】:

    在我看来,您从 OOP 领域描述的解决方案在 FP 领域听起来不错。也就是说:

    data UID = UID
        { _time :: Integer
        , _word :: String
        }
    

    在此记录中包含或不包含“印章”是一项设计决策,我们可能没有足够的信息来回答这里。可以把这个数据类型放到自己的模块中,定义一个“智能构造器”和“智能访问器”:

    uid = UID
    time = _time
    word = _word
    

    然后在模块边界隐藏真正的构造函数和访问器,例如导出UID 类型、uid 智能构造函数和timeword 智能访问器,但不导出UID 构造函数或_time_word 访问器。

    module UID (UID, uid, time, word) where
    

    如果我们后来发现智能构造函数应该做一些处理,我们可以改变uid的定义:

    uid t w = UID t (processWord w)
    

    【讨论】:

      【解决方案3】:

      在 Nikita Vokov 的回答之上,您可以使用 record wild cards 获得一些简洁的语法,几乎没有重复:

      {-# LANGUAGE RecordWildCards #-}
      
      data FileNames = FileNames { photo :: String, audio :: String, dir :: String }
      
      buildFileNames :: Word -> Time -> Stamp -> FileNames
      buildFileNames time word stamp = FileNames
        (show word ++ "-" ++ show time ++ show stamp)
        (show word ++ "-" ++ show time ++ ".mp3")
        (show word ++ "_" ++ show time )
      
      let FileNames {...} = buildFileNames time wrd stamp
      in ... photo ... audio ... dir...
      

      【讨论】:

      • @user1436026 在此示例中,将FileNames 视为您的OOP 对象,将buildFileNames 视为您的构造函数。 photoaudiodir这三个记录是公共属性。它们在您第一次访问时计算一次。
      【解决方案4】:

      再举一个例子,如果你将相同的参数传递给多个函数,你可以改用 Reader monad:

      import Control.Monad.Reader
      
      runR = flip runReader
      
      type Params = (String, String, String)
      
      buildPhotoFileName :: Reader Params String
      buildPhotoFileName = do
        (time, word, stamp) <- ask
        return $ show word ++ "-" ++ show time ++ show stamp
      
      main = do
        runR (time, word, stamp) $ do
          photo <- buildPhotoFileName
          audio <- buildAudioFileName
          dir <- buildDirectoryName
          processStuff photo audio dir
      

      【讨论】:

      • 使你的函数基于 Reader 会带走函数的许多不错的属性,如柯里化等。
      • 当然可以,但是你会得到很多不错的属性作为回报。
      【解决方案5】:

      要基于 David Wagner 的解决方案和您的 OO 目标,您应该将 buildxxx 函数或多个函数移至单独的模块(NameBuilders?),这样您就可以完全控制。

      即使采用这种方法,您也应该按照 David 的建议,用模块内的函数“包装”变量。

      您将导出变量和 buildxxx 构造函数(返回一个三元组)或构造函数(三个单独的函数)。

      你也可以简化为

          buildDirectoryName time word = show word ++ "_" ++ show time
          buildPhotoFileName stamp = buildDirectoryName + show stamp
          buildAudioFileName =       buildDirectoryName ++ ".mp3"
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-24
        • 1970-01-01
        • 1970-01-01
        • 2017-11-14
        • 1970-01-01
        相关资源
        最近更新 更多