Keyword arguments in Haskell
by Edward Z. Yang
Keyword arguments are generally considered a good thing by language designers: positional arguments are prone to errors of transposition, and it’s absolutely no fun trying to guess what the 37 that is the third argument of a function actually means. Python is one language that makes extensive use of keyword arguments; they have the following properties:
- Functions are permitted to be a mix of positional and keyword arguments (a nod to the compactness of positional arguments),
- Keywords are local to any given function; you can reuse a named function argument for another function,
- In Python 3.0, you can force certain arguments to only be specifiable with a keyword.
Does Haskell have keyword arguments? In many ways, they’re much less necessary due to the static type system: if you accidentally interpose an Int and Bool your compiler will let you know about it. The type signature guides you!
Still, if we were to insist (perhaps our function took many arguments of the same type), one possibility is to pass a record data type in as the sole argument, but this is a little different than Python keyword arguments in the following ways:
- There is a strict delineation between positional and keywords: either you can specify your record entirely with keywords or entirely with positional arguments, but you can’t do both,
- Record fields go into the global namespace, so you have to prefix/suffix them with some unique identifier, and
- Even with named records, a user can still choose to construct the record without specifying keyword arguments. For large argument lists, this is not as much of an issue, but for short argument lists, the temptation is great.
I find issue two to be the reason why I don’t really employ this trick; I would find it quite annoying to have to make a data structure for every function that I wanted to use named arguments with.
I’d like to suggest another trick to simulate named arguments: use newtypes! Consider this undertyped function:
renderBox :: Int -> Int -> Int -> Int -> IO () renderBox x y width height = undefined main = renderBox 2 4 50 60
We can convert it to use newtypes like this:
newtype XPos = XPos Int newtype YPos = YPos Int newtype Width = Width Int newtype Height = Height Int renderBox :: XPos -> YPos -> Width -> Height -> IO () renderBox (XPos x) (YPos y) (Width width) (Height height) = undefined main = renderBox (XPos 2) (YPos 4) (Width 50) (Height 60)
Unlike the usual use of newtypes, our newtypes are extremely short-lived: they last just long enough to get into the body of renderBox and then they are pattern matched to oblivion: the function body can rely on good local variable names to do the rest. But it still manages to achieve the goals of keyword arguments: any call to renderBox makes it crystal clear what each integer means. We also maintain the following good properties:
- If the type already says all you need to say about an argument, there’s no need to newtype it again. Thus, you can have a mix of regular and newtype arguments.
- Newtypes can be reused. Even further, they are only to be reused when the semantic content of their insides is the same, which encourages good naming practices.
- The user is forced to do the newtype wrapping: there’s no way around it. If you publish smart constructors instead of the usual constructors, you can factor out validation too.
Newtypes are so versatile!
Did you enjoy this post? Please subscribe to my feed!
I like SML’s anonymous records, with which the above Python features are all possible. In my opinion, this is Haskell’s missing feature. But perhaps Haskell should be Haskell. :-)
Good advice, newtypes are well suited to this purpose, and I really do wish that there was a way to not make record element names appear in the global scope; it is especially apparent when I have two different data types that should have the same record name.
Very Cool advice, a code like that is great to follow =)
Using newtypes like this is tidy but misses a couple of key features of python/haskell record style keyword arguments:
– flexible argument ordering
– the ability to specify default values, used when a keyword is not specified.
The second of these is important enough to me that I use haskell records to simulate keyword args, but the single namespace for record fields is a significant irritation.
Tim: You could use a Default class:
class Default d where
default :: d
And then instantiate defaults for each newtype.
If you think you need keyword arguments, you need to fix your function. A function should never need more than two arguments, three is the max. A function with more arguments usually is a sign that you need a datatype to hold some of the arguments which belong together.
renderBox should obviously be a function with one argument: a box.
Eyal:
Agreed, you could use a typeclass for defaulting, but this means you’d end up with code like:
renderBox (XPos 2) (YPos 4) default default
which misses out on a key benefit of defaults – not having to mention them. Sometime I want to make an API extensible, adding function parameters without needing to change existing call sites. Using records is the only way I know to do this in haskell.
Default values are also useful (see http://www.haskell.org/haskellwiki/Default_values_in_records).
data Foo = Foo { bar :: Int, baz :: Int, quux :: Int }
fooDefault = Foo { bar = 1, baz = 2, quux = 3 }
newRecord = fooDefault { quux = 42 }
I’m fairly new to Haskell, and I’m not really following why you’re using a newtype instead of a data. (I’m generally not sure what the purpose of newtype is.)
I understand that a data declaration requires a separate type and constructor, but it still seems like it would work the same.
Hello Ben. Newtypes are a restricted form of data declarations (only one constructor is allowed and it may only have one field), but the benefit is that they are optimized away at compile time, so it really is just a static check.