有没有一种很好的方法可以让函数签名在Haskell中更丰富?

我意识到这可能被认为是一种主观的或者是一个脱离主题的问题,所以我希望它不会被封闭,而是会被迁移,也许会被程序员所接受。

我开始学习Haskell,主要是为了我自己的启发,我喜欢支持这种语言的很多想法和原则。 在参加Lisp的语言理论课之后,我开始对函数式语言着迷,而且我听到很多关于Haskell生产效率的好消息,所以我想我会自己调查一下。 到目前为止,我喜欢这种语言,除了我无法摆脱的一件事:那些母亲正在执行功能签名。

我的专业背景主要是做OO,特别是在Java中。 我工作过的大多数地方都有许多标准的现代教条, 敏捷,Clean Code,TDD等。经过几年这样的工作,它已经成为我的舒适区; 特别是“好”的代码应该是自我记录的想法。 我已经习惯了在IDE中工作,在这个IDE中,具有非常具有描述性签名的冗长冗长的方法名称对于智能自动完成和用于导航程序包和符号的大量分析工具来说是不成问题的; 如果我可以在Eclipse中按下Ctrl + Space,然后通过查看其名称和与其参数相关联的局部范围变量而不是拉起JavaDocs来推断出一种方法正在做什么,我就像在粪便中的猪一样快乐。

显然,这不是哈斯克尔社区最佳实践的一部分。 我已经阅读了很多关于此事的不同意见,并且我知道Haskell社区认为它的简洁性是“专业”。 我已经阅读了如何阅读Haskell,并且我理解了很多决定背后的基本原理,但这并不意味着我喜欢它们。 一个字母变量名等对我来说并不好玩。 我承认,如果我想继续使用该语言,我必须习惯这一点。

但我无法克服功能签名。 以这个例子为例,从学习Haskell [...]的函数语法部分:

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"  
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"  
    | otherwise                   = "You're a whale, congratulations!"

我意识到这是一个愚蠢的例子,它只是为了解释守卫和类的限制而创建的,但是如果你只是研究这个函数的签名,你就不会知道它的哪个论点会成为权重或身高。 即使你使用FloatDouble而不是任何类型,它仍然不会立即可辨。

起初,我认为我会变得可爱,聪明,聪明,并尝试使用具有多个类别约束的更长类型的变量名称来欺骗它:

bmiTell :: (RealFloat weight, RealFloat height) => weight -> height -> String

这吐出了一个错误(如果有人能向我解释错误,我会很感激):

Could not deduce (height ~ weight)
    from the context (RealFloat weight, RealFloat height)
      bound by the type signature for
                 bmiTell :: (RealFloat weight, RealFloat height) =>
                            weight -> height -> String
      at example.hs:(25,1)-(27,27)
      `height' is a rigid type variable bound by
               the type signature for
                 bmiTell :: (RealFloat weight, RealFloat height) =>
                            weight -> height -> String
               at example.hs:25:1
      `weight' is a rigid type variable bound by
               the type signature for
                 bmiTell :: (RealFloat weight, RealFloat height) =>
                            weight -> height -> String
               at example.hs:25:1
    In the first argument of `(^)', namely `height'
    In the second argument of `(/)', namely `height ^ 2'
    In the first argument of `(<=)', namely `weight / height ^ 2'

我并没有完全理解为什么这种方法不起作用,我开始使用Google搜索,甚至发现了这个小帖子,它建议命名参数,特别是通过newtype欺骗命名参数,但这似乎有点多。

有没有可接受的方法来制作信息功能签名? “哈斯克尔之道”简单来说就是把所有的东西都废弃了吗?


类型签名不是Java风格的签名。 Java风格的签名会告诉你哪个参数是重量,哪个参数是高度,因为它使用参数类型混合了参数名称。 Haskell无法做到这一点,因为函数是使用模式匹配和多个方程来定义的,如下所示:

map :: (a -> b) -> [a] -> [b]
map f (x:xs) = f x : map f xs
map _ [] = []

这里第一个参数在第一个方程中被命名为f ,而第二个参数中的_ (这几乎意味着“未命名”)。 第二个参数在两个方程中都没有名称; 在它的第一部分有名称(程序员可能会认为它是“xs列表”),而第二部分则是完全字面表达式。

然后有一些无点的定义,如:

concat :: [[a]] -> [a]
concat = foldr (++) []

类型签名告诉我们它需要一个类型为[[a]]参数,但该参数的名称不会出现在系统中的任何位置

除了作为函数的单个方程之外,它用来引用其参数的名称无论如何都是不相关的, 除了文档。 由于函数参数的“规范名称”的思想在Haskell中没有很好的定义,因此“ bmiTell的第一个参数表示重量,而第二个参数表示高度”的信息位于文档中,而不是类型签名中。

我绝对同意,一个功能所做的事情应该从可用的“公共”信息中清楚地看到。 在Java中,这是函数的名称,参数类型和名称。 如果(通常情况下)用户需要更多的信息,请将其添加到文档中。 在Haskell中,有关函数的公共信息是函数名称和参数类型。 如果用户需要比这更多的信息,则将其添加到文档中。 注意Hasksell的IDE(如Leksah)很容易向你展示Haddock的评论。


请注意,像Haskell这样具有强大表现力类型系统的语言,首选做的事情就是尝试尽可能多地检测出类型错误的错误。 因此,像bmiTell这样的功能bmiTell立即向我发出警告标志,原因如下:

  • 它需要两个代表不同事物的相同类型的参数
  • 如果以错误的顺序传递参数,它会做错误的事情
  • 这两种类型没有一个自然的位置(因为++的两个[a]参数)
  • 通常为了提高类型安全性而做的一件事情的确是制造新类型,就像你发现的链接一样。 我并不认为这与命名参数传递有很大关系,更重要的是它需要创建一个明确表示高度的数据类型,而不是用数字来衡量的任何其他数量。 所以我不会只在呼叫时出现新的值; 无论从哪里获取高度数据,我都会使用newtype值,并将其作为高度数据而不是数字传递,以便在任何地方都能获得类型安全(和文档)优势。 当我需要将值传递给数字而不是高度的东西时(例如bmiTell的算术运算),我只会将这个值解开为原始数值。

    请注意,这没有运行时间开销; newtypes与newtype wrapper中的数据“内部”相同,所以wrap / unwrap操作对底层表示没有任何操作,并且在编译过程中被简单地删除。 它仅在源代码中添加额外的字符,但这些字符正是您正在寻找的文档,并且由编译器强制执行的额外好处; Java风格的签名告诉你哪个参数是重量,哪个是高度,但是编译器仍然无法判断你是否意外地通过了错误的方式!


    还有其他的选择,取决于你想要用你的类型得到多么愚蠢和/或迂腐。

    例如,你可以这样做...

    type Meaning a b = a
    
    bmiTell :: (RealFloat a) => a `Meaning` weight -> a `Meaning` height -> String  
    bmiTell weight height = -- etc.
    

    ......但这非常愚蠢,可能会令人困惑,并且在大多数情况下都无济于事。 这同样适用于此,此外还需要使用语言扩展:

    bmiTell :: (RealFloat weight, RealFloat height, weight ~ height) 
            => weight -> height -> String  
    bmiTell weight height = -- etc.
    

    稍微更明智的是:

    type Weight a = a
    type Height a = a
    
    bmiTell :: (RealFloat a) => Weight a -> Height a -> String  
    bmiTell weight height = -- etc.
    

    ...但这仍然有点愚蠢,并且在GHC扩展类型同义词时往往会迷路。

    这里真正的问题是你将附加的语义内容附加到相同多态类型的不同值上,这违背了语言本身的粒度,因此通常不是惯用的。

    当然,一种选择是只处理无信息类型的变量。 但是,如果两种相同类型的东西之间存在明显的区别,那么它们的顺序并不明显,但这并不令人满意。

    我建议你尝试使用newtype包装来指定语义:

    newtype Weight a = Weight { getWeight :: a }
    newtype Height a = Height { getHeight :: a }
    
    bmiTell :: (RealFloat a) => Weight a -> Height a -> String  
    bmiTell (Weight weight) (Height height)
    

    我认为,这样做是远远不够的。 这是一个额外的输入(哈,哈),但它不仅使您的类型签名更具信息性,即使扩展了同类型同义词,也可以让类型检查器在错误地使用重量作为高度时捕获等等。 使用GeneralizedNewtypeDeriving扩展,即使对于通常无法派生的类型类,您甚至可以获得自动实例。


    海德克斯和/或也在看函数方程(你绑定的东西的名字)是我告诉发生了什么的方法。 你可以像Haddock这样的个人参数,

    bmiTell :: (RealFloat a) => a      -- ^ your weight
                             -> a      -- ^ your height
                             -> String -- ^ what I'd think about that
    

    所以它不只是一个文本解释所有的东西。

    你可爱的类型变量不起作用的原因是你的功能是:

    (RealFloat a) => a -> a -> String
    

    但是你的尝试改变:

    (RealFloat weight, RealFloat height) => weight -> height -> String
    

    相当于这样:

    (RealFloat a, RealFloat b) => a -> b -> String
    

    所以,在这种类型的签名中,你说前两个参数有不同的类型,但GHC已经确定(基于你的使用)它们必须具有相同的类型。 所以它抱怨说它不能确定weightheight是相同的类型,即使它们必须是(也就是说,你提出的类型签名不够严格并且会允许功能的无效使用)。

    链接地址: http://www.djcxy.com/p/80777.html

    上一篇: Is there a nice way to make function signatures more informative in Haskell?

    下一篇: Prolog vs. Haskell