有没有一种很好的方法可以让函数签名在Haskell中提供更多信息?

Dou*_*hen 52 syntax haskell functional-programming coding-style

我意识到这可能被认为是一个主观或可能是一个非主题的问题,所以我希望它不会被关闭,而是会被迁移,也许是程序员.

我开始学习Haskell,主要是为了我自己的教化,我喜欢支持语言的很多想法和原则.在参加了我们玩Lisp的语言理论课之后,我对函数式语言着迷了,而且我听说过很多关于Haskell有多高效的好东西,所以我想我会自己调查一下.到目前为止,我喜欢这种语言,除了一件我无法摆脱的事情:那些母亲正在起作用的功能签名.

我的专业背景主要是做OO,特别是在Java中.我工作过的大多数地方都在许多标准的现代教条中受到重创; 敏捷,清洁代码,TDD等.经过几年的工作,它一定成为我的舒适区; 尤其是"好"代码应该是自我记录的想法.我已经习惯了在IDE中工作,其中具有非常描述性签名的冗长和详细的方法名称对于智能自动完成和用于导航包和符号的大量分析工具来说不是问题; 如果我可以在Eclipse中按Ctrl + Space,那么从查看其名称和与其参数关联的本地范围变量而不是拉起JavaDocs推断出方法正在做什么,我和大便中的猪一样高兴.

这显然不是Haskell社区最佳实践的一部分.我已经阅读了很多关于此事的不同意见,我理解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!"
Run Code Online (Sandbox Code Playgroud)

我知道这是只用于解释警卫和类约束的目的而创建一个愚蠢的例子,但如果你要检查只是该函数的签名,你根本不知道它的它的参数是意欲在重或高度.即使你使用FloatDouble代替任何类型,它仍然不会立即可辨别.

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

bmiTell :: (RealFloat weight, RealFloat height) => weight -> height -> String
Run Code Online (Sandbox Code Playgroud)

这吐了一个错误(顺便说一句,如果有人能向我解释错误,我将不胜感激):

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'
Run Code Online (Sandbox Code Playgroud)

不完全理解为什么不起作用,我开始谷歌搜索,我甚至发现这个小帖子建议命名参数,特别是欺骗命名参数通过newtype,但这似乎有点多.

有没有可接受的方法来制作信息功能签名?"哈斯克尔之路"只是对哈多克的一切废话吗?

Ben*_*Ben 81

类型签名不是Java样式的签名.Java样式的签名将告诉您哪个参数是权重,哪个是高度,因为它将参数名称与参数类型混合在一起.Haskell不能将此作为一般规则,因为函数是使用模式匹配和多个方程定义的,如:

map :: (a -> b) -> [a] -> [b]
map f (x:xs) = f x : map f xs
map _ [] = []
Run Code Online (Sandbox Code Playgroud)

这里第一个参数f在第一个等式中命名,并且_(在第二个中,它几乎意味着"未命名").第二个参数不具有任何方程中的名称; 在它的第一部分有名称(程序员可能会把它想象成"xs列表"),而在第二部分它是一个完全文字的表达.

然后是无点定义,如:

concat :: [[a]] -> [a]
concat = foldr (++) []
Run Code Online (Sandbox Code Playgroud)

类型签名告诉我们它接受一个类型的参数[[a]],但该参数的名称不会出现在系统的任何地方.

在函数的单个等式之外,除了作为文档之外,它用于引用其参数的名称无关紧要.由于函数参数的"规范名称"的概念在Haskell中没有很好地定义,因此信息的位置" bmiTell表示权重的第一个参数,而第二个表示高度"在文档中,而不是在类型签名中.

我完全同意,功能所做的事情应该从关于它的"公共"信息中清楚地表达出来.在Java中,这是函数的名称,以及参数类型和名称.如果(通常)用户需要更多信息,请将其添加到文档中.在Haskell中,有关函数的公共信息是函数的名称和参数类型.如果用户需要更多信息,请将其添加到文档中.注意Haskell的IDE(如Leksah)将很容易向您显示Haddock注释.


请注意,在具有强大且富有表现力的类型系统(如Haskell)的语言中,首选的方法是尝试尽可能多地将错误视为类型错误.因此,像bmiTell我一样立即向我发出警告标志的功能,原因如下:

  1. 它需要两个相同类型的参数代表不同的东西
  2. 如果以错误的顺序传递参数,它将做错误的事情
  3. 这两种类型没有自然的位置(作为两个[a]参数++)

通常用于增加类型安全性的一件事就是制作新类型,就像你找到的链接一样.我并不认为这与命名参数传递有很大关系,更多的是关于创建一个明确表示高度的数据类型,而不是您可能想用数字测量的任何其他数量.所以我不会只在呼叫时出现newtype值; 我将使用newtype值,无论我哪里获得高度数据,并将其作为高度数据而不是数字传递,以便我在各处获得类型安全(和文档)的好处.当我需要将值传递给操作数字而非高度(例如内部算术运算bmiTell)的东西时,我只会将值解包为原始数字.

请注意,这没有运行时开销; newtypes与newtype包装器"内部"的数据表示相同,因此wrap/unwrap操作在底层表示中是no-ops,并且在编译期间被简单地删除.它只在源代码中添加了额外的字符,但这些字符正是您正在寻找的文档,还有编译器强制执行的额外好处; Java风格的签名告诉你哪个参数是权重,哪个是高度,但编译器仍然无法判断你是否意外地以错误的方式传递它们!

  • 为了得到优秀的解释+1,为什么我们不能这样做Java,为什么类型安全是要走的路 - 可读性作为更重要的正确性的副作用 - 道格担心他可能不确定哪个方向,Ben说使编译器以正确的方式强制执行.优秀. (13认同)
  • @DougStephen:作为一名多年来一直使用C#的行业程序员,他的学术背景有限,并且在空闲时间学习Haskell以获得乐趣 - 这些是完全的,令人难以置信的*可怕的*实用编码指南太多方面对我来说,在评论中列出.使代码读成英文?*真的吗?*我们没有从先前的错误中学到*任何东西*吗? (12认同)
  • @DougStephen:是的,这些指南中的根本错误是认为它们实际上使代码更容易以有意义的方式理解.他们没有.英语不够精确,无论如何都不能描述非平凡的代码,而更长的名字在阅读代码时具有更高的认知负荷.更不用说90%的完整描述通常比10%更完整的描述更具误导性,因为它更难以注意到它所做的比它声称的更多.对不起,但他们真的太可怕了. (10认同)
  • +1因为当我在大学学习Haskell时,我讨厌它 - 但我不仅理解并且喜欢你的解释,我实际上感觉自己爱了Haskell一会儿. (4认同)

C. *_*ann 37

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

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

type Meaning a b = a

bmiTell :: (RealFloat a) => a `Meaning` weight -> a `Meaning` height -> String  
bmiTell weight height = -- etc.
Run Code Online (Sandbox Code Playgroud)

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

bmiTell :: (RealFloat weight, RealFloat height, weight ~ height) 
        => weight -> height -> String  
bmiTell weight height = -- etc.
Run Code Online (Sandbox Code Playgroud)

这将是更明智的:

type Weight a = a
type Height a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String  
bmiTell weight height = -- etc.
Run Code Online (Sandbox Code Playgroud)

......但是当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)
Run Code Online (Sandbox Code Playgroud)

我认为,做到这一点并不像我们应有的那样普遍.这是一个额外的打字(ha,ha),但它不仅使你的类型签名更具信息性,即使扩展了类型同义词,它还允许类型检查器捕获,如果你错误地使用权重作为高度,或类似.使用GeneralizedNewtypeDeriving扩展,您甚至可以获得自动实例,即使对于通常无法派生的类型类也是如此.

  • @MichaelLitchard我插入自己的博客:http://joyoftypes.blogspot.com/2012/08/generalizednewtypederiving-is.html (4认同)
  • @JohnL:不是那个扩展的粉丝,我接受了吗?对于`newtype`s用作美化类型的同义词,对我来说似乎是合理的... (3认同)
  • @JohnL:如果要为每个扩展分配一个从0到1的值,这样1是纯粹的仁慈,0是完全恶意的,"IncoherentInstances"将位于大约-0.28 + 0.96i. (3认同)
  • 我认为newtype不应该是"Weight",而是一个合适的单位,比如说"kg".这样你就可以准确地知道它应该包含什么,当你将它与其他单位组合时你会得到什么,你可以自然地添加一个距离和一个长度.这可能听起来像挑剔,但想到像"睡觉(时间a)"之类的东西.时间是几分钟,几秒钟还是其他什么? (2认同)

sin*_*yma 27

Haddocks和/或也在查看函数方程(你绑定的名字)是我告诉你发生了什么的方式.你可以Haddock个人参数,像这样,

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

所以它不只是一大堆文字解释所有的东西.

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

(RealFloat a) => a -> a -> String
Run Code Online (Sandbox Code Playgroud)

但你的尝试改变了:

(RealFloat weight, RealFloat height) => weight -> height -> String
Run Code Online (Sandbox Code Playgroud)

相当于:

(RealFloat a, RealFloat b) => a -> b -> String
Run Code Online (Sandbox Code Playgroud)

所以,在这种类型的签名中你已经说过前两个参数有不同的类型,但是GHC已经确定(根据你的用法)它们必须具有相同的类型.因此,它抱怨它无法确定weight并且height是相同的类型,即使它们必须是(也就是说,您提出的类型签名不够严格并且允许无效使用该函数).

  • 为[Haddock个人参数] +1(http://www.haskell.org/haddock/doc/html/ch03s02.html#id565220).虽然我仍然会推荐`newtype`方法. (2认同)

And*_*ewC 14

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

{-# 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!"
Run Code Online (Sandbox Code Playgroud)

但是,这也不理想.你必须记住,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!"
Run Code Online (Sandbox Code Playgroud)

我相信,这是你在OOP中明确表达的方式.我真的不相信你正在使用你的OOP方法参数的类型来获取这些信息,你必须秘密使用参数名称而不是类型,并且期望haskell告诉你参数名称是不公平的当你排除在你的问题中读取参数名称时.[见*下面] Haskell中的类型系统非常灵活且非常强大,请不要因为它最初疏远你而放弃它.

如果您真的希望类型告诉您,我们可以为您做到:

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
....
Run Code Online (Sandbox Code Playgroud)

这是表示文件名的字符串使用的方法,因此我们定义

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

这给你的清晰度.然而,感觉到了

type FilePath = String
Run Code Online (Sandbox Code Playgroud)

缺乏类型安全,那

newtype FilePath = FilePath String
Run Code Online (Sandbox Code Playgroud)

或者更聪明的东西会是一个更好的主意.关于类型安全的非常重要的一点,请参阅Ben的答案.

[*]好的,你可以这样做:在ghci中获取没有参数名称的类型签名,但是ghci用于源代码的交互式开发.你的库或模块不应该没有文档和hacky,你应该使用非常轻量级的语法haddock文档系统并在本地安装haddock.更合理的投诉版本是没有:v命令打印函数bmiTell的源代码.度量标准表明,相同问题的Haskell代码将缩短一个因子(在我的情况下,我发现与等效的OO或非oo命令式代码相比大约为10),因此在gchi中显示定义通常是明智的.我们应该提交功能请求.

  • Upvoted,因为'Person`记录是更好的方法,恕我直言.即使你已经引入了新的类型来区分身高和体重,爱丽丝的体重除以理查德的身高也不是BMI. (4认同)
  • 我喜欢你的解释,但觉得"更好的方法"是将记录_和_"newtype" - 这样,你们两个1)关联同一个人的体重/身高; 2)使用类型系统确保您使用的是重量和高度,而不仅仅是两个任意的浮动/ RealFloat类型数字. (2认同)

Gab*_*lez 13

试试这个:

type Height a = a
type Weight a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String
Run Code Online (Sandbox Code Playgroud)


Mat*_*hid 12

可能与具有piffling两个参数的函数无关,但是 ......如果你有一个函数需要大量的参数,类似的类型或者只是不清楚的排序,那么定义一个表示它们的数据结构可能是值得的.例如,

data Body a = Body {weight, height :: a}

bmiTell :: (RealFloat a) => Body a -> String
Run Code Online (Sandbox Code Playgroud)

你现在也可以写

bmiTell (Body {weight = 5, height = 2})
Run Code Online (Sandbox Code Playgroud)

要么

bmiTell (Body {height = 2, weight = 5})
Run Code Online (Sandbox Code Playgroud)

并且这两种方式都是正确的,并且对任何想要阅读代码的人来说都是显而易见的.

不过,对于具有大量参数的函数来说,它可能更值得.对于只有两个,我会和其他人一起使用newtype,所以类型签名会记录正确的参数顺序,如果你混淆它们会得到一个编译时错误.