53

I'm used to being able to define optional arguments like so in Python:

def product(a, b=2):
    return a * b

Haskell doesn't have default arguments, but I was able to get something similar by using a Maybe:

product a (Just b) = a * b
product a Nothing = a * 2

This becomes cumbersome very quickly if you have more than multiple parameters though. For example, what if I want to do something like this:

def multiProduct (a, b=10, c=20, d=30):
    return a * b * c * d

I would have to have eight definitions of multiProduct to account for all cases.

Instead, I decided to go with this:

multiProduct req1 opt1 opt2 opt3 = req1 * opt1' * opt2' * opt3'
    where opt1' = if isJust opt1 then (fromJust opt1) else 10
    where opt2' = if isJust opt2 then (fromJust opt2) else 20
    where opt3' = if isJust opt3 then (fromJust opt3) else 30

That looks very inelegant to me. Is there an idiomatic way to do this in Haskell that is cleaner?

Chris Stryczynski
  • 19,899
  • 28
  • 104
  • 198
Vlad the Impala
  • 14,192
  • 14
  • 66
  • 116
  • 2
    So how many parameters does a function *really* take? ;-) –  Oct 15 '11 at 22:40
  • I don't think it's the *exact* same question so I won't vote to close, but this is pretty similar to http://stackoverflow.com/questions/2790860/optional-arguments-in-haskell – MatrixFrog Oct 15 '11 at 22:47
  • 1
    @MatrixFrog This question is about arguments for functions. That question is about values for data types. – Vlad the Impala Oct 15 '11 at 22:51
  • It may make sense to change your function to take a data structure containing four numbers, instead of taking the four numbers separately. In that case, you can use the solution from that other question. – MatrixFrog Oct 15 '11 at 22:52

6 Answers6

86

Perhaps some nice notation would be easier on the eyes:

(//) :: Maybe a -> a -> a
Just x  // _ = x
Nothing // y = y
-- basically fromMaybe, just want to be transparent

multiProduct req1 opt1 opt2 opt3 = req1 * (opt1 // 10) * (opt2 // 20) * (opt3 // 30)

If you need to use the parameters more than once, I suggest going with @pat's method.

EDIT 6 years later

With ViewPatterns you can put the defaults on the left.

{-# LANGUAGE ViewPatterns #-}

import Data.Maybe (fromMaybe)

def :: a -> Maybe a -> a
def = fromMaybe

multiProduct :: Int -> Maybe Int -> Maybe Int -> Maybe Int -> Int
multiProduct req1 (def 10 -> opt1) (def 20 -> opt2) (def 30 -> opt3)
  = req1 * opt1 * opt2 * opt3
luqui
  • 57,324
  • 7
  • 134
  • 191
  • 16
    basically, `(//) = flip fromMaybe` :-) I like that you picked the same operator as Perl. – pat Oct 16 '11 at 02:47
  • 3
    I like it because it looks like `||` just slanted a little :) – Dan Burton Oct 19 '11 at 18:46
  • 4
    @pat, perl is my heritage :-) – luqui Oct 21 '11 at 22:05
  • 3
    Out of interest, this is defined in Control.Error.Util as '?:', similar to coffeescript's '?' operator – unohoo Apr 30 '15 at 08:40
  • @unohoo Groovy's Elvis! – Czipperz Jul 08 '15 at 18:06
  • 1
    Also the same operator as Elixir. – Dogweather Aug 20 '16 at 08:47
  • Is it correct that the caller needs to use `Just` for specified optional parameters, and `Nothing` in place of parameters that it does not wish to specify? E.g. `multiProduct 1 (Just 2) Nothing Nothing`. Is there a way (using this technique) to end up with call syntax like `multiProduct 1 2` or `multiProduct 1 2 3`? – davidA May 09 '20 at 00:07
  • @davidA Look at rampion's answer for that technique. But I agree with his addendum that this is not something you should use in practice, it interacts poorly with type inference and will get ugly. – luqui May 09 '20 at 04:05
37

Here's yet another way to do optional arguments in Haskell:

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FlexibleContexts #-}
module Optional where

class Optional1 a b r where 
  opt1 :: (a -> b) -> a -> r

instance Optional1 a b b where
  opt1 = id

instance Optional1 a b (a -> b) where
  opt1 = const

class Optional2 a b c r where 
  opt2 :: (a -> b -> c) -> a -> b -> r

instance Optional2 a b c c where
  opt2 = id

instance (Optional1 b c r) => Optional2 a b c (a -> r) where
  opt2 f _ b = \a -> opt1 (f a) b

{- Optional3, Optional4, etc defined similarly -}

Then

{-# LANGUAGE FlexibleContexts #-}
module Main where
import Optional

foo :: (Optional2 Int Char String r) => r
foo = opt2 replicate 3 'f'

_5 :: Int
_5 = 5

main = do
  putStrLn $ foo        -- prints "fff"
  putStrLn $ foo _5     -- prints "fffff"
  putStrLn $ foo _5 'y' -- prints "yyyyy"

Update: Whoops, I got accepted. I honestly think that luqui's answer is the best one here:

  • the type is clear, and easy to read, even for beginners
  • same for type errors
  • GHC doesn't need hints to do type inference with it (try opt2 replicate 3 'f' in ghci to see what I mean)
  • the optional arguments are order-independent
Community
  • 1
  • 1
rampion
  • 82,104
  • 41
  • 185
  • 301
18

I don't know of a better way to solve the underlying problem, but your example can be written more succinctly as:

multiProduct req1 opt1 opt2 opt3 = req1 * opt1' * opt2' * opt3'
    where opt1' = fromMaybe 10 opt1
          opt2' = fromMaybe 20 opt2
          opt3' = fromMaybe 30 opt3
pat
  • 12,213
  • 1
  • 21
  • 49
7

When arguments get too complex, one solution is to create a data type just for the arguments. Then you can create a default constructor for that type, and fill in only what you want to replace in your function calls.

Example:

$ runhaskell dog.hs 
Snoopy (Beagle): Ruff!
Snoopy (Beagle): Ruff!
Wishbone (Terrier): Ruff!
Wishbone (Terrier): Ruff!
Wishbone (Terrier): Ruff!

dog.hs:

#!/usr/bin/env runhaskell

import Control.Monad (replicateM_)

data Dog = Dog {
        name :: String,
        breed :: String,
        barks :: Int
    }

defaultDog :: Dog
defaultDog = Dog {
        name = "Dog",
        breed = "Beagle",
        barks = 2
    }

bark :: Dog -> IO ()
bark dog = replicateM_ (barks dog) $ putStrLn $ (name dog) ++ " (" ++ (breed dog) ++ "): Ruff!"

main :: IO ()
main = do
    bark $ defaultDog {
            name = "Snoopy",
            barks = 2
        }

    bark $ defaultDog {
            name = "Wishbone",
            breed = "Terrier",
            barks = 3
        }
mcandre
  • 19,306
  • 17
  • 77
  • 140
2

A possible improvement/modification on the record-approach mentioned by mcandre and Ionuț, is to use lenses:

{-# LANGUAGE -XTemplateHaskell #-}

data Dog = Dog {
  _name :: String,
  _breed :: String,
  _barks :: Int
}

makeLenses ''Dog

defaultDog :: Dog
defaultDog = Dog {
  _name = "Dog",
  _breed = "Beagle",
  _barks = 2
}

bark :: (Dog -> Dog) -> IO ()
bark modDog = do
  let dog = modDog defaultDog
  replicateM_ (barks dog) $ putStrLn $
    (name dog) ++ " (" ++ (breed dog) ++ "): Ruff!"

main :: IO ()
main = do
  bark $ (name .~ "Snoopy") . (barks .~ 2)
  bark $ (name .~ "Wishbone") . (breed .~ "Terrier") . (barks .~ 3)

Or alternatively

bark :: Dog -> IO ()
bark dog = do
  replicateM_ (barks dog) $ putStrLn $
    (name dog) ++ " (" ++ (breed dog) ++ "): Ruff!"

main :: IO ()
main = do
  bark $ name .~ "Snoopy" $ barks .~ 2 $ defaultDog
  bark $ name .~ "Wishbone" $ breed .~ "Terrier" $ barks .~ 3 $ defaultDog

See here for the meaning of (.~).

dremodaris
  • 345
  • 1
  • 8
1

Here is a way that makes Implicit Parameters look like optional arguments:

{-# LANGUAGE Rank2Types, ImplicitParams #-}

multiProduct :: (Num x) => x -> ((?b::x) => x) -> ((?c::x) => x) -> ((?d::x) => x) -> x
multiProduct a b c d = let ?b=10 ; ?c=20 ; ?d=30 
    in  a * b * c * d

test1 = multiProduct 1 ?b ?c ?d  -- 6000
test2 = multiProduct 2 3 4 5     -- 120