如何参数化我的Haskell函数?

时间:2013-12-28 23:22:50

标签: design-patterns haskell monads state-monad

我希望我的术语在这里是正确的 - 如果没有,请随时编辑任何内容。

我正在使用Haskell作为辅助语言,同时撰写关于组合博弈论的论文 - 也就是说,我的所有函数都只是玩杂耍数字并帮助我找到我正在研究的游戏的解决方案。

我写了一个具体的,整体'板尺寸'的所有功能(想想chequerboard,5x5等)。我想扩展到任何规模的研究板,因此通过包含Integer参数来修改所有相关功能。 e.g。

type Size = Int

squares :: Size -> [Square]
squares size = [ (x,y) | x <- [1..size],  
                         y <- [1..size]]
然而,这已经开始变得混乱。通常我认为与大小无关的函数必须在访问需要大小的函数时提供大小。

这很快就会产生这样的界限:

reaching size = (losing size) \\\ (setA size)

takeUDmir _  []       = []
takeUDmir size (x:xs) = [x] ++ takeUDmir size (xs \\\ mirUD size x)

rotate size s = concatMap (mirUD size) (mirLR size s)

(请注意,这些功能的内容确实无关紧要,我只是想表明它已经失控了。)

我非常有信心使用Haskell,并且通常使用函数式编程,但我不知道如何删除所有这些size引用,并简单地依赖于其他设置每个需要使用它的功能的大小。

我想我可能正在寻找一个单子,但我不知道。

4 个答案:

答案 0 :(得分:12)

这是一个完美的时间来提取Reader monad-it摘要一些全局可用的只读配置数据的概念。

data Config = Config { size :: Int, ... }

type MyMonad = Reader Config

fun :: MyMonad Result
fun = funNeedingTheSize <$> asks size <*> pure arg1 <*> pure arg2

runMyMonad :: Config -> MyMonad a -> a
runMyMonad = flip runReader

答案 1 :(得分:4)

我要在这里充实奥古斯都的建议。如果您只需要定义size一次,那么您可以在本地绑定中构建所有依赖机制。这有效地允许您在size的特定定义的上下文中创建许多函数,然后使用所有这些函数只需选择size一次。它与RecordWildCards

特别配合
{-# LANGUAGE RecordWildCards -#}

data Methods =
  Methods { meth1 :: Int -> Int
          , meth2 :: Int -> Int
          ...
          }

mkMethods :: Int -- ^ size
          -> Methods
mkMethods size = 
  Methods { meth1 = \i -> size + i
          , meth2 = \i -> size - i
          ...
          }

...

someFn :: Int -> Result
someFn size = ... meth1 ... meth2 ...
  where Methods {..} = mkMethods size

这实际上是模拟ML模块的有限方法。

答案 2 :(得分:3)

Reader monad的替代方法是Implicit Parameters

如果您尝试在递归调用中更改参数的值,语义可能会很微妙,这些通常会导致错误。

然而,它们确实具有避免需要以monadic风格重写代码的优势,这是一种极具侵略性的改变,有损于可读性。

我的观点是,它们在像你这样的情况下是完美的,它们只会在一系列调用的顶层设置为一种“配置”信息,而不会在该链中发生变化。

如果您有类型签名,它们仍然会侵入您的类型签名,但如果您不提供签名,它们会像类型类一样自动推断。

如果您在size处使用隐式参数,则上面的代码段看起来像这样:

type Size = Int

squares :: (?size :: Size) => [Square]
squares = [ (x,y) | x <- [1..?size],  
                    y <- [1..?size]]

reaching = losing \\\ setA

takeUDmir []     = []
takeUDmir (x:xs) = [x] ++ takeUDmir (xs \\\ mirUD x)

rotate s = concatMap mirUD (mirLR s)

可能在某些函数中,显式传递值更合适 - 它取决于size参数对该特定函数的重要程度。

要实际“实例化”参数,请使用let

let ?size = 5 in squares

答案 3 :(得分:1)

解决此问题的另一种方法是使用reflection库。这绝对是一种复杂的方法,应该非常谨慎地使用。 Austin Seipp available at FP Complete上有一个教程资源可用于实现,但这可能非常多毛。

reflection的简化形式是Given类型类。为此,我们只需使用given在我们需要某个全局配置值的地方注释我们的函数。

meth1 = 1 + given
meth2 = given - 1

ImplicitParams一样,我们注意到我们在类型签名中使用了Given

meth1 :: (Num a, Given a) => a
meth2 :: (Num a, Given a) => a

这允许我们静态消除它们并确保使用give传递所有正确的配置信息

give :: a -> (Given a => r) -> r

因此,如果我们有someComplexCalculation :: Given Config => Result,我们可以通过提供Config来解决这个问题。

give config someComplexCalculation :: Result

并且类型确保我们不会让这个丢失的配置传播到顶部。


Reifies / reflect / reify机制概括了这个属性,但却失去了一些方便和简单。如果必须传递多个重叠的配置,则需要使用Given系统无法知道哪些given应由重叠区域下的传递配置替换Reifies使用s参数解决该问题,与ST monad的s参数非常相似。

Reifies的杀手级应用程序将运行时配置的类型类传递给表达式。通常你不能覆盖类型类,但Reifies提供了一种技巧。有关详细信息,请参阅Austin Seipp的底部教程。