Tuesday, April 08, 2008

Optional Parameters in Haskell

I use optional parameters in my TagSoup library, but it seems not to be a commonly known trick, as someone recently asked if the relevant line was a syntax error. So, here is how to pass optional parameters to a Haskell function.

Optional Parameters in Other Languages

Optional parameters are in a number of other languages, and come in a variety of flavours. Ada and Visual Basic both provide named and positional optional parameters. For example, given the definition:


Sub Foo(b as Boolean = True, i as Integer = 0, s as String = "Hello")


We can make the calls:


Call Foo(s = "Goodbye", b = False)
Call Foo(False, 1)


In the first case we give named parameters, in the second we give all the parameters up to a certain position.

In some languages, such as GP+, you can say which parameters should take their default values:


Call Foo(_, 42, _)


Optional Parameters in Haskell

Haskell doesn't have built-in optional parameters, but using the record syntax, it is simple to encode named optional parameters.


data Foo = Foo {b :: Bool, i :: Integer, s :: String}
defFoo = Foo True 0 "Hello"

foo :: Foo -> IO ()


Now we can pass arguments by name, for example:


foo defFoo{s = "Goodbye", b = False}


This syntax takes the value defFoo, and replaces the fields s and b with the associated values. Using a type class, we can abstract this slightly:


class Def a where
def :: a

instance Def Foo where
def = defFoo


Now we can make all functions taking default arguments use the def argument as a basis, allowing type inference and type classes to choose the correct default type. Even still, optional parameters in Haskell are not quite as neat as in other languages, but the other features of Haskell mean they are required less often.

This technique has been used in TagSoup, particularly for the parseTagOptions function. I've also seen this technique used in cpphs with the runCpphs function.

12 comments:

David R. MacIver said...

Surely using this is going to give you severe namespacing headaches?

Neil Mitchell said...

David: If you use s, b and i - then definitely! For TagSoup I deliberately use long names, such as "optTagPosition". The namespace issue is a general problem with records, which as yet has no good solution.

David R. MacIver said...

Right. But given that it's a general problem, introducing a large number of new record fields doesn't seem like a good idea, so this is something to be used sparingly at best. :-)

I realised that you didn't intend things like s, b, and i to be actually used for this, but I find that most cases where one would want default arguments it would be natural to have fairly brief descriptive names.

It's still a nice trick though. I've used a very similar one in other languages.

Anonymous said...

I use this for hvac too -- picked up the idiom from the hughespj prettyprint library. namespacing isn't so bad though, just a bit ugly with all the qualifiers.

Anonymous said...

I've use records for defaults a fair bit in my chart library. The typeclass is new to me, and nice. Next API change, I think I'll make use of that also.

Anonymous said...

I should also note that the Def class is exactly like Hughes' Sat class from Restricted Datatypes in Haskell. I wonder if like Box (Box a = Box a) and seal (Seal = Seal a), Sat/Def isn't so general it really belongs in some standard library instance.

Anonymous said...

Hello, I'm new to haskell but isn't the definition of Def should be with class and not instance?

class Def a where
def :: a

Neil Mitchell said...

sclv: I'd make it One, data One a = One a, and add it to Data.Tuple - then it fits neatly in the standard libraries. Having Def in the standard libraries would be useful, but I'm not sure where you'd put it.

anon: You are indeed correct, I have updated the blog with your modification.

Unknown said...

{- Thanks, I did not know this is possible! For those who like to copy and paste, here is a working demonstration (ghc wants "Bool" not "Boolean"): -}

class Def a where def :: a

instance Def Foo where def = defFoo

data Foo = Foo {b :: Bool, i :: Integer, s :: String} deriving Show

defFoo = Foo True 0 "Hello"

foo :: Foo -> IO ()
foo = putStrLn . show

main = do { foo def; foo def {s = "Goodbye", b = False}}

-- It's a pity that comments here disallow pre/code blocks...

Neil Mitchell said...

Anton: woops, I typed Boolean for VB, then forgot that its Bool in Haskell - now fixed. Thanks for making the code available as a long snippet.

Ivan Z. said...

There is also a different implementation of named parameters (keyword arguments) (which is more sophisticated in the implementation mechanism) described there.

Quotation:

Also in marked contrast with records, keyword labels may be reused throughout the code with no restriction; the same label may be associated with arguments of different types in different functions. Labels of Haskell records may not be re-used.

Anonymous said...

Fascinatingly, the code example in Unknown's 12:33 PM comment runs correctly when compiled with GHC and when wrapped in multi-line delimiters in GHCI (":{...}:"), but not entered on individual lines in GHCI.