6
votes

I have read a book called Clean code. One of the strongest messages that I took from the book is that the code must be readable. I do not understand why functional languages such as F# do not include function parameter names into the intellisense or in the type defintion?

Compare

val copy: string -> string -> unit

with

val copy: sourceFile:string -> destinationFile:string -> unit

What is the best practise in the functional world? Shall we prefer single parameter functions? This is what Clean code promotes. Or shall we use records for all functions of 2+ parameters?

I know that one work-around is to use static member instead of let functions but this is not a functional approach, is it?

EDIT:

Just to give more information:

  • Haskell : addThree :: Int -> Int -> Int -> Int
  • OCaml: Unix.unlink: (string -> unit)

and surely others. They just do not show parameter names in the type definitions.

4
What version of the language are you using? In F# 3.0, typing let f x y = x + y + 1 into F# interactive gives me: val f : x:int -> y:int -> int (and IntelliSense gives me the same thing in the editor).kvb
this is true, but how about: let g() = let f x y = x + y + 1 in (). Anything that is not at the top level does not work. But it is good to know that F# team tries to do something about it ;) I was just confused by the general nature of the functional programming. Or do other functional languages show parameter names?Oldrich Svec
This is about IDEs and development environments, not languages.Don Stewart
In Haskell at least, pattern matching makes named parameters in the type signature often impractical. foo Nothing = bar; foo (Just []) = baz; foo (Just (x:xs)) = quux. What should the IDE make of that? Also, foldl' :: (a -> b -> a) -> a -> [b] -> a; foldl' f z = go z where go ..., only some arguments are explicitly provided in the definition.Daniel Fischer
"Shall we prefer single parameter functions?" <- That's how it is in Haskell, every function takes exactly one argument. addThree :: Int -> Int -> Int -> Int is a function taking an Int and returning (a function taking an Int and returning (a function taking an Int and returning an Int)). [Of course that's cumbersome, so one normally speaks of a function taking three arguments, but occasionally, the difference is important.]Daniel Fischer

4 Answers

7
votes

If you practice typeful programming, which is the idea that a lot of the semantic content of programs can be reflected statically in the type system, you will find out that in many (but not all) cases, named arguments are not necessary for readability.

Consider the following examples in the List standard library of OCaml. By knowing that they operate on lists, and with the (hopefully clear: we're all for good name choices) name of the function, you will find that you don't need explanations for what the arguments do.

val append : α list -> α list
val flatten : α list list -> α list
val exists: (α -> α bool) -> α list -> bool
val map: (α -> β) -> α list -> β list
val combine : α list -> β list -> (α * β) list

Note that the last example is interesting because it is not exactly clear what the code will do. There would in fact be several interpretations. combine [1;2] [3;4] returns [(1,3); (2,4)] and not, for example, [(1,3); (1,4); (2,3); (2,4)]. It is also not clear what happens when the two lists are not of the same length (the failure mode is unclear).

Functions that are not total, that may raise an exception or not terminate, usually need more documentation about what the failure cases are and how they will behave. This is one strong argument in favor of what we call pure programming, where all the behavior of a function is expressed in terms of returning a value (no exceptions, observable state mutation, or non-termination), and can therefore be statically captured by the type system.

Of course this only works really well for functions that are parametric enough; they have a type that make it very clear what they do. This is not the case of all functions, consider for example the blit function of the String module (I'm sure your favorite language has such examples as well):

val blit : string -> int -> string -> int -> int -> unit

huh?

Programming languages add support named parameters for this reason. In OCaml for example we have "labels" that allow to name parameters. The same function is exported in the StringLabels module as:

val blit : src:string -> src_pos:int -> dst:string -> dst_pos:int -> len:int -> unit

That's better. Yes, named parameters are useful in some cases.

Note however that named arguments can be used to hide bad API design (maybe the example above is targe to this criticism as well). Consider:

val add : float -> float -> float -> float -> float -> float -> float * float * float

obscure, huh? But then:

type coord = {x:float; y:float; z:float}
val add : coord -> coord -> coord

That's much better, and I didn't need any parameter labeling (arguably record labels provide naming, but in fact I could equally use float * float * float here; the fact that value records may subsume named (and optionals?) parameters is also an interesting remark).

David M. Barbour develops the argument that named parameters are a "crutch" of language design, that is used to tamper over the lazyness of API designers, and that not having them encourages better design. I am not sure I agree that named parameters can be profitably avoided in all situations, but he certainly has a point that agrees with the typeful propaganda at the beginning of my post. By raising the level of abstraction (through more polymorphic/parametric types or better problem domain abstractions), you'll find you decrease the need for parameter naming.

6
votes

Or am I completely wrong and it has nothing to do with diferences between functional and imperative programming?

You're not completely wrong, insofar as functional languages with HM type inference often do not require type annotations at all, or, at least not everywhere.

Add to this that type expressions are not necessarily function types, hence the concept of a "parameter name" is not applicable. All in all, a name there is just redundant, it does not add any information to a type, and that could be the reason for not allowing it.

Conversely, in imperative languages, type inference was almost unknown as of late. Thus you must declare everything (in statically typed languages) and so it happens that name and type tend to appear in one place. Moreover, as functions are not first class citiziens, the concept of a function type, let alone an expression that describes a function type is merely unknown.

Observe that with recent developments (like "lambda" syntax, etc.) the concept of an argument whose type is known already or can be easily inferred also appears in those languages. And when I remember correctly, there is even syntactical easement to avoid long names, the lambda argument is just it or even _

6
votes

Haskell's type synonyms can help in making the type signatures more self-documenting. Consider for example the function writeFile, that just writes a string to a file. It has two parameters: the string to write, and the filename to write to. Or was it the other way around? Both parameters are of type String, so it's not easy to tell which is which!

However, when you look at the documentation, you see the following type signature:

writeFile :: FilePath -> String -> IO ()

That makes it clear (to me, at least!) how the function is intended to be used. Now, since FilePath is just a synonym for String, there's nothing preventing you from using it like this:

writeFile "The quick brown fox jumped over the lazy dog" "test.txt"

but if you get the type FilePath -> String -> IO () as a hint in your IDE, I think that's at least a big push in the right direction!

You could even go a step further and make a newtype for filepaths so you don't accidentally mix up filenames and contents, but I guess that adds more hassle than it's worth, and there's probably also historical reasons why this isn't done.

4
votes

What is the best practise in the functional world?

There is alternative standard library for OCaml called Core. It uses labeled parameters almost everywhere. For example

val fold_left : 'a t -> init:'b -> f:('b -> 'a -> 'b) -> 'b

P.S. I have no information about another functional languages.