Is there a good way to make feature signatures more informative in Haskell?

I understand that this can potentially be considered as a subjective or, perhaps, an off-topic question, so I hope that instead of closing it, it will be transferred, possibly to programmers.

I am starting to learn Haskell, mainly for my own edification, and I like a lot of ideas and principles that support the language. I became fascinated by functional languages ​​after taking the language theory class where we played with Lisp, and I heard a lot of good things about how productive Haskell can be, so I decided that I would research it myself. So far I have liked this language, with the exception of one thing that I can’t just get away from: these signatures are functions of the mother operation.

My professional background mainly does OO, especially in Java. Most of the places I worked in have scored a lot of standard modern dogma; Agile, Clean Code, TDD, etc. After several years working this way, he definitely became my comfort zone; especially the idea that "good" code should be self-documenting. I'm used to working in an IDE where long and verbose method names with very descriptive signatures are not an issue with smart auto-completion and a huge set of analytic tools for navigating packages and characters; if I can press Ctrl + Space in Eclipse, then output what the method does by looking at its name and the locally restricted variables associated with its arguments, instead of raising JavaDocs, I am as happy as a pig in the feed.

This, of course, is not part of the Haskell community best practices. I read a lot of different opinions on this issue, and I understand that the Haskell community considers its conciseness "pro." I went through How to read Haskell , and I understand the rationale for many decisions, but that does not mean that I like them; variable names in one letter, etc., I'm not interested. I admit that I will have to get used to this if I want to continue to crack my tongue.

But I can not get function labels. Take this example, as pulled from the Learn about the Haskell section [...] about function syntax:

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!" 

I understand that this is a stupid example that was created only to explain the guards and class restrictions, but if you only looked at the signature of this function, you would not understand which of its arguments were intended to be weight or height. Even if you used Float or Double instead of any type, it still would not be immediately distinguishable.

At first, I thought I would be cute, smart, and brilliant, and try to fake it using longer variable type names with a few class restrictions:

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

This spat out an error (aside, if anyone could explain the error to me, I would be grateful):

 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' 

Not fully understanding why this did not work, I started Googling and I even found this little post that offers named parameters, in particular spoofing named parameters via newtype , but that seems a bit big.

Is there an acceptable way to handle signatures of information functions? Is "Haskell's Way" just to get the shit out of everything?

+50
syntax coding-style functional-programming haskell
Sep 14 2018-12-12T00:
source share
6 answers

A type signature is not a Java style signature. A Java-style signature will tell you which parameter is weight and which is height, only because it mixes parameter names with parameter types. Haskell cannot do this as a general rule, because functions are defined using pattern matching and several equations, as in:

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

Here, the first parameter has the name f in the first equation and _ (which means "unnamed" in many ways) in the second. The second parameter has no name in any of the equations; there are names in the first parts (and the programmer will probably think of it as a "xs list"), and in the second this is a completely literal expression.

And then there are point definitions like:

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

The type signature tells us that it accepts a parameter that is of type [[a]] , but the name for this parameter does not appear anywhere in the system.

Outside the bounds of a separate equation for a function, the names that it uses to denote its arguments are in any case irrelevant except . Since the idea of ​​a "canonical name" for a function parameter is not defined in Haskell, the place for the information "the first parameter is bmiTell represents weight and the second represents height" is in the documentation, not in the signature type.

I absolutely agree that the function should be completely transparent from the "publicly available" information available about it. In Java, this is the name of the function, as well as types and parameter names. If (as usual) the user needs additional information, add it to the documentation. In Haskell, publicly available function information is the name of the function and parameter types. If the user needs additional information, add him to the documentation. Note. Haskell IDEs like Leksah will easily show you Haddock comments.




Note that the preferred thing in a language with a strong and expressive type system, such as Haskell, often tries to make as many mistakes as possible, detect as type errors. Thus, a function like bmiTell immediately displays warning signs for the following reasons:

  • Two parameters of the same type are required, representing different things.
  • This will do the wrong thing if the parameters passed in the wrong order
  • The two types have no natural position (since the two arguments [a] for ++ do)

One thing that is often done to increase type safety is really to create new types, as in the link you found. I really don’t think of it as having much in common with passing a named parameter, especially since we are talking about creating a data type that explicitly represents the height , and not any other value that you can measure with the number. Therefore, I would not have the newtype values ​​that appear only when called; I would use the newtype value wherever I got height data, and also pass it as height data, not a number, so that I get security benefits (and documentation) around the world. I would only expand the value to the original number when I need to pass it to something that works with numbers, and not in height (for example, arithmetic operations inside bmiTell ).

Note that this has no overhead at runtime; newtypes are presented identically to the data "inside" the newtype wrapper, so the wrap / flip operations are no-ops in the base view and are simply deleted at compile time. It only adds extra characters to the source code, but these characters are exactly what you are looking for, with the added benefit provided by the compiler; Java-style signatures tell you which parameter is weight and which is height, but the compiler will still not be able to tell if you accidentally gave them the wrong path!

+77
Sep 14
source share

There are other options, depending on how stupid and / or pedantic you want to get with your types.

For example, you can do this ...

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

... but it is incredibly stupid, potentially confusing and in most cases does not help. The same for this, which additionally requires the use of language extensions:

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

It would be a little more reasonable:

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

... but it still looks silly and tends to get lost when the GHC extends type synonyms.

The real problem is that you add additional semantic content to different values ​​of the same polymorphic type, which goes against the grain of the language itself and, as such, is usually not idiomatic.

One option, of course, is simply to deal with uninformative type variables. But this is not very satisfactory if there is a significant difference between two things of the same type that are not obvious in the order in which they are indicated.

Instead, I recommend that you try using newtype wrappers to indicate semantics:

 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) 

Doing it nowhere is as wide as it deserves, I think. This is a bit extra typing (ha, ha), but it not only makes your type signatures more informative, even with the extension of type synonyms, which allows you to check the type if you mistakenly use weight as height or as such. With the extension GeneralizedNewtypeDeriving you can even get automatic instances even for type classes that normally cannot be obtained.

+36
Sep 14
source share

Helpers and / or functions that also look at the equation (the names you linked) are the ways in which I tell what happens. You can use separate parameters, for example,

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

so this is not just a piece of text explaining all things.

The reason your cute type variables are not working is because your function:

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

But your attempted change:

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

equivalent to this:

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

So, in a signature of this type, you said that the first two arguments are of different types, but the GHC has determined that (based on your use) they must be of the same type. Therefore, he complains that he cannot determine that weight and height are the same type, even if they should be (i.e. your proposed type signature is not strict enough and allows for invalid use of the function).

+26
Sep 14 '12 at
source share

weight must be of the same type as height , because you divide them (without implicit throws). weight ~ height means they are of the same type. ghc explained a little how he came to the conclusion that weight ~ height was necessary, sorry. You can say that he / you wanted to use the syntax from the extension of the type family:

 {-# 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!" 

However, this is also not ideal. You must remember that Haskell does use a completely different paradigm, and you must be careful not to assume that what is important in another language is important. You learn most when you are out of your comfort zone. It’s like someone from London rises to Toronto and complains about the city, it’s confusing because all the streets are the same, and someone from Toronto may demand that London bewildered because there are no streets regularity. What you call obfuscation is called Haskellers clarity.

If you want to return to more object-oriented clarity of purpose, then make bmiTell work only for people, therefore

 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!" 

This, I believe, is how you clarify this in OOP. I really do not believe that you are using the OOP method argument type to get this information, you should secretly use parameter names for clarity, not types, and you can hardly expect that haskell will tell you the parameter names when you exclude the reading of parameter names in your issue. [cm. * below] The Haskell type system is remarkably flexible and very powerful, please do not abandon it just because it initially alienates you.

If you really want the types to tell you, we can do it for you:

 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 .... 

What is the approach used with strings that represent file names, so we define

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

which gives the clarity that you were. However, he believed that

 type FilePath = String 

lack of security type and that

 newtype FilePath = FilePath String 

or something even smarter would be a much better idea. See Ben's Answer for a very important type safety question.

[*] Well, you can do: t in ghci and get a type signature without a parameter name, but ghci is for interactive source code development. Your library or module should not remain undocumented and hacked, you should use an incredibly lightweight syntax documentation system for buffering and establish a local area network. A more legitimate version of your complaint would be that there is no command: v that prints the source code for your bmiTell function. Metrics assume that your Haskell code for the same problem will be much shorter (I find about 10 in my case compared to equivalent OO or incompatible imperative code), so defining a definition inside gchi is often reasonable. We must provide a feature request.

+14
Sep 14
source share

It may not apply to a function with two arguments, but ... If you have a function that takes many arguments, similar types, or just fuzzy ordering, it might be worthwhile to define the data structure that represents them. For example,

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

Now you can write either

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

or

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

and it will cost correctly in both directions, as well as be obvious to anyone trying to read your code.

This is probably worth more for functions with more arguments. In just two, I would go with everyone else and just newtype , so that the type signature documents the correct order of the parameters, and you get a compile-time error if you mix them.

+12
Sep 14 '12 at 15:41
source share

Try the following:

 type Height a = a type Weight a = a bmiTell :: (RealFloat a) => Weight a -> Height a -> String 
+11
Sep 14
source share



All Articles