8

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

我在写一篇关于组合博弈论的论文时使用 Haskell 作为辅助语言 - 即,我的所有函数都只是处理数字并帮助我为我正在研究的游戏找到解决方案。

我为一个具体的、完整的“棋盘大小”(想想棋盘、5x5 等)编写了所有函数。我想扩展到任何大小的学习板,所以通过包含一个整数参数来修改所有相关函数。例如

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

4 回答 4

12

这是提取Readermonad 的最佳时机——它抽象了一些全局可用的只读配置数据的概念。

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
于 2013-12-28T23:33:38.517 回答
4

我将在这里充实 Augustss 的建议。如果您只需要定义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 模块的有限方法。

于 2013-12-29T17:20:37.620 回答
3

Reader monad 的替代方法是隐式参数

如果您尝试在递归调用中更改参数的值,这些通常会受到负面影响,因为语义可能很微妙。

但是,它们确实具有避免以 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
于 2013-12-29T13:28:51.190 回答
1

解决此问题的另一种方法是使用reflection库。这绝对是一种复杂的方法,应该非常小心地使用。Austin Seipp在 FP Complete上提供了一个关于它的教程资源,用于实现它,但这可能非常麻烦。

的简化形式reflectionGiven类型类。为此,我们只需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//机器概括了这个属性reflectreify但这样做却失去了一些方便和简单。如果您必须传递多个重叠的配置,则需要它 - 使用Given系统不可能知道哪些givens 应该被重叠区域下的传递配置替换,使用非常类似于monad 的参数的参数Reifies来解决这个问题。sSTs

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

于 2013-12-29T17:24:10.557 回答