Sometimes in functional programming communities you will see a design recommendation like the following:
- Don’t just habitually use
Maybe
and Result
everywhere. Instead use custom data types that capture the business/application logic.
For example, here’s an excerpt from the Elm language guide that advises against over using Maybe
:
For example, say we have an exercise app where we compete against our friends. You start with a list of your friend’s names, but you can load more fitness information about them later. You might be tempted to model it like this:
type alias Friend =
{ name : String
, age : Maybe Int
, height : Maybe Float
, weight : Maybe Float
}
All the information is there, but you are not really modeling the way your particular application works. It would be much more precise to model it like this instead:
type Friend
= Less String
| More String Info
type alias Info =
{ age : Int
, height : Float
, weight : Float
}
This new model is capturing much more about your application. There are only two real situations. Either you have just the name, or you have the name and a bunch of information.
I’m going to translate this into Julia, with an extra twist.
abstract type Friend end
struct Unknown end
struct PreferNotToSay end
struct PartialFriend <: Friend
name::String
end
struct Info
age::Int
height::Float64
weight::Float64
end
struct ShyInfo
age::Int
height::Float64
end
struct NotShyFriend <: Friend
name::String
info::Info
end
struct ShyFriend <: Friend
name::String
info::ShyInfo
end
name(f::PartialFriend) = f.name
name(f::NotShyFriend) = f.name
name(f::ShyFriend) = f.name
weight(::PartialFriend) = Unknown()
weight(::ShyFriend) = PreferNotToSay()
weight(f::NotShyFriend) = f.info.weight
_print_weight(name, ::Unknown) = println("$name's weight is unknown.")
_print_weight(name, ::PreferNotToSay) = println("$name prefers not to disclose their weight.")
_print_weight(name, weight::Float64) = println("$name's weight is $weight.")
print_weight(f::Friend) = _print_weight(name(f), weight(f))
Note how the _print_weight
methods dispatch on the different possible return types of the weight
function. This is pretty similar to pattern matching on a sum type in a case statement.
The individual weight
methods are actually type-stable, but the return type of a call to weight
in some code might be a union type, like when you call weight
on an object from a heterogenous collection, like this,
friends = # A `Vector{Friend}`
weight(friends[2])
where the return type of weight(friends[2])
is
Union{Float64, PreferNotToSay, Unknown}
There are known performance issues with heterogeneous collections in Julia, but semantically the design pattern I’ve outlined in the example above is quite similar to the design approach in a functional language with sum types (i.e. create custom data types that represent the business logic—don’t just habitually use Maybe
and Result
). One of the main reasons to use sum types in Julia is to avoid the performance pitfalls of heterogeneous collections. If you really need sum types, take a look at the new Moshi.jl library.