Monday, June 15, 2015

optional-args-1.0.0: Optional function arguments

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

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

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.

1 comment:

  1. There is a useful library that tries to solve this problem called data-default. It defines a class

    class Default a where
    def = a

    With many predefined instances.