I'm releasing a small library named optional-args
to simplify functions that take optional arguments.
Traditionally you would represent an optional argument using Maybe
, like this:
greet :: Maybe String -> String
greet (Just name) = "Hello, " ++ name
greet Nothing = "Hello"
Then you would provide a specific value by wrapping the value in pure
or Just
:
>>> greet (pure "John")
"Hello, John"
... or you can use the default by suppling empty
or Nothing
:
>>> greet empty
"Hello"
The disadvantage to this approach is that Maybe
does not implement the IsString
, Num
, or Fractional
type classes, meaning that you cannot use numeric or string literals in place of Maybe
. You have to explicitly wrap them in pure
or Just
.
The optional-args
library provides an Optional
type that is equivalent to Maybe
, but with additional instances for IsString
, Num
, and Fractional
:
data Optional a = Default | Specific a
instance IsString a => IsString (Optional a)
instance Num a => Num (Optional a)
instance Fractional a => Fractional (Optional a)
Additionally, the constructors are better-named for the task of defining function arguments:
import Data.Optional
greet :: Optional String -> String
greet (Specific name) = "Hello, " ++ name
greet Default = "Hello"
Since Optional
implements IsString
, you can use a naked string literal anywhere a function expects an Optional
string argument as long as you enable the OverloadedStrings
extensions:
>>> :set -XOverloadedStrings
>>> greet "John"
"Hello, John"
... and you can still use either Default
or empty
to use the default:
>>> greet empty
"Hello"
Similarly, any function that accepts an Optional
numeric argument:
birthday :: Optional Int -> String
birthday (Specific age) = "You are " ++ show age ++ " years old!"
birthday Default = "You are one year older!"
... will accept naked numeric literals when you invoke the function:
>>> birthday 20
"You are 20 years old!"
For values that are not literals, you can still wrap them in pure
:
>>> let age = 20
>>> birthday (pure age)
"You are 20 years old!"
The IsString
, Num
, and Fractional
instances are recursive, so you can wrap your types in a more descriptive newtype and derive IsString
or Num
:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Data.Optional
import Data.String (IsString)
newtype Name = Name { getName :: String } deriving (IsString)
greet :: Optional Name -> String
greet (Specific name) = "Hello, " ++ getName name
greet Default = "Hello"
newtype Age = Age { getAge :: Int } deriving (Num)
birthday :: Optional Age -> String
birthday (Specific age) = "You are " ++ show (getAge age) ++ " years old!"
birthday Default = "You are one year older!"
... and you would still be able to use naked numeric or string literals as function arguments:
>>> :set -XOverloadedStrings
>>> greet "John"
"Hello, John"
>>> birthday 20
"You are 20 years old!"
The Optional
type's Monoid
instance also plays nicely with the IsString
instance. Specifically, Optional
obeys these two laws:
fromString (str1 <> str2) = fromString str1 <> fromString str2
fromString mempty = mempty
... which is a fancy way of saying that this code:
>>> greet ("John " <> "Smith")
"Hello, John Smith"
... will behave the same as this code:
>>> greet "John Smith"
"Hello, John Smith"
Even if we were to implement an IsString
instance for Maybe
, it would not satisfy the second law.
The Optional
type is also the only Maybe
-like type I know of that has a recursive Monoid
instance:
instance Monoid a => Monoid (Optional a)
These sorts of recursive instances come in handy when chaining Monoid
instances as illustrated in my post on scaling equational reasoning.
The optional-args
library also comes with documentation explaining intended usage in detail (almost identical to this post), so if the user clicks on the Optional
type they will discover clear instructions on how to use the type.
If you would like to use this, you can find the library on Hackage or Github.
There is a useful library that tries to solve this problem called data-default. It defines a class
ReplyDeleteclass Default a where
def = a
With many predefined instances.