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:

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

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

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

    ReplyDelete
  4. Anonymous9:50 PM

    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.

    ReplyDelete
  5. Anonymous7:32 AM

    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.

    ReplyDelete
  6. Anonymous9:15 AM

    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.

    ReplyDelete
  7. Anonymous9:29 AM

    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

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

    ReplyDelete
  9. {- 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...

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

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

    ReplyDelete
  12. Anonymous5:34 PM

    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.

    ReplyDelete