在Haskell中有没有一种很好的方法来使函数签名更加丰富?

我意识到这可能被认为是一个主观的或者是一个偏离主题的问题,所以我希望不要把它closures,否则会被迁移,也许会被程序员所接受。

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

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

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

但我无法克服function签名。 拿这个例子来说,从学习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!" 

我意识到这是一个愚蠢的例子,只是为了解释守卫和阶级约束的目的而创build的,但是如果你只是研究那个函数的签名,你就不会知道它的哪个论点是权重或高度。 即使你使用Float或者Double来代替任何types,它仍然不能立即被识别出来。

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

 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' 

不完全理解为什么没有工作,我开始使用谷歌search,我甚至发现这个小post,build议命名参数,特别是通过newtype欺骗命名参数 ,但似乎有点多。

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

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

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

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

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

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

types签名告诉我们它需要一个types为[[a]]参数,但是这个参数的名字不会出现在系统的任何地方

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

我绝对同意,从现有的“公共”信息中,应该清楚地看到一个function的作用。 在Java中,这是函数的名称,参数types和名称。 如果(如常见)用户需要比这更多的信息,则将其添加到文档中。 在Haskell中,有关函数的公共信息是函数的名称和参数types。 如果用户需要比这更多的信息,则将其添加到文档中。 注意Hasksell的IDE(例如Leksah)很容易向你展示Haddock的评论。


请注意,像Haskell这样具有强大performance力types系统的语言所要做的事情,往往是尝试尽可能多的错误来检测types错误。 因此,像bmiTell这样的functionbmiTell立即向我发出警告,原因如下:

  1. 它需要两个相同types的参数来表示不同的东西
  2. 如果以错误的顺序传递参数,它会做错误的事情
  3. 这两种types没有一个自然的位置(因为++的两个参数)

在增加types安全方面,经常做的一件事情就是制作新的types,就像你find的链接一样。 我并不认为这与命名parameter passing有很大关系,更重要的是要制作明确表示高度的数据types,而不是使用数字来衡量的任何其他数量。 所以我不会只在呼叫中出现新的值; 无论哪里获取高度数据我都会使用newtype值,并将其作为高度数据而不是数字传递,以便我可以在任何地方获得types安全(和文档)好处。 当我需要传递给数字而不是高度的东西(比如bmiTell的算术运算)时,我只会将这个值打开成一个原始数值。

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

还有其他的select,取决于你想用你的types如何愚蠢和/或迂腐。

例如,你可以做到这一点…

 type Meaning ab = 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扩展types同义词时,这仍然有点愚蠢,并且往往会迷失方向。

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

当然,一种select是只处理无形的typesvariables。 但是,如果两种相同types的东西之间存在明显的区别,那么它们就不是很明显。

我build议你尝试,而是使用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) 

这样做是远远不够的,我认为,值得。 这是一个额外的打字(哈,哈),但它不仅使您的types签名更丰富,即使扩展types同义词,它可以让types检查器捕获,如果你错误地使用重量作为一个高度,或这样的。 使用GeneralizedNewtypeDeriving扩展,即使对于通常不能派生的types类,甚至可以获得自动实例。

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

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

所以这不仅仅是一个文本解释所有的东西。

你可爱的typesvariables不起作用的原因是你的function是:

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

但是你的尝试改变:

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

相当于这个:

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

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

weight必须与height相同,因为你将它们分开(没有隐式转换)。 weight ~ height意味着它们是相同的types。 ghc已经解释了如何得出weight ~ height是必要的结论,抱歉。 您可以告诉它使用types系列扩展的语法:

 {-# LANGUAGE TypeFamilies #-} bmiTell :: (RealFloat weight, RealFloat height,weight~height) => weight -> height -> 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!" 

但是,这也不是理想的。 你必须记住,Haskell实际上使用了一个非常不同的范例,你必须小心,不要认为在这里重要的是另一种语言。 当你在舒适区之外时,你正在学习的最多。 就像伦敦的一个人在多伦多出现,抱怨这座城市令人困惑,因为所有的街道都是一样的,而来自多伦多的人可能会声称伦敦是混乱的,因为街道上没有规律。 你称之为混淆的东西被Haskellers称为清晰。

如果你想回到更多的面向对象的目的明确,那么使bmiTell工作在人,所以

 data Person = Person {name :: String, weight :: Float, height :: Float} bmiOffence :: Person -> String bmiOffence p | weight p / height p ^ 2 <= 18.5 = "You're underweight, you emo, you!" | weight p / height p ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!" | weight p / height p ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!" | otherwise = "You're a whale, congratulations!" 

我相信,这是你在面向对象程序中明确expression的方式。 我真的不相信你正在使用你的OOP方法参数的types来获得这些信息,你必须暗中使用参数名称来清晰而不是types,而且期望Haskell告诉你参数名称是不公平的当你排除阅读你的问题中的参数名称[见*下面]在Haskell中的types系统是非常灵活和非常强大的,请不要放弃它只是因为它最初疏远你。

如果你真的想要types告诉你,我们可以为你做到这一点:

 type Weight = Float -- a type synonym - Float and Weight are exactly the same type, but human-readably different type Height = Float bmiClear :: Weight -> Height -> String .... 

这是代表文件名的string使用的方法,所以我们定义

 type FilePath = String writeFile :: FilePath -> String -> IO () -- take the path, the contents, and make an IO operation 

这给你以后的清晰度。 然而,这是感觉到的

 type FilePath = String 

缺乏types安全,那

 newtype FilePath = FilePath String 

或者更聪明的东西将是一个更好的主意。 关于types安全,见Ben的回答非常重要。

[*]好的,你可以这样做:在ghci中获取没有参数名称的types签名,但是ghci是用于交互式开发源代码的。 你的库或模块不应该保持无文档和hacky,你应该使用令人难以置信的轻量级语法haddock文档系统,并在本地安装haddock。 您的投诉更合理的版本将是没有:v命令打印您的functionbmiTell的源代码。 度量标准表明,同样问题的Haskell代码将会缩短一个因素(我发现与同等的OO或非OO命令代码相比,我的代码约为10),因此在gchi中显示定义通常是合理的。 我们应该提交一个function请求。

如果你有一个函数需要大量的参数,相似的types或不清楚的顺序,那么可能需要定义一个代表它们的数据结构。 例如,

 data Body a = Body {weight, height :: a} bmiTell :: (RealFloat a) => Body a -> String 

你现在可以写

 bmiTell (Body {weight = 5, height = 2}) 

要么

 bmiTell (Body {height = 2, weight = 5}) 

而且这两种方式都是正确的,而且对任何试图阅读代码的人都是显而易见的。

不过,这对于具有更多参数的函数来说可能更值得。 只有两个,我会与其他人一起,只是新types,所以types签名文件正确的参数顺序,你会得到一个编译时错误,如果你把它们混合起来。

尝试这个:

 type Height a = a type Weight a = a bmiTell :: (RealFloat a) => Weight a -> Height a -> String