This is an experimental package with functions that works with the following types of monads:
- Maybe
- Either / Result
- List
The fmap function can map over any Maybe monad (either Just or None).
If the input is wrapped as a Just object, the output is automatically
wrapped as well. NONE is a singleton constant of None.
Unlike other implementations, a fmap'ed function can also take ordinary values
rather than monads. It would then apply the function as usual and return
an ordinary result. So the result is not elevated to a wrapped monad.
1 |> fmap(x -> x + 1) # 2
just(1) |> fmap(x -> x + 1) # Just(2)
NONE |> fmap(x -> x + 1) # NONEUse or_else to switch over to a useful value when NONE is encountered.
1 |> or_else(2) # 1
just(1) |> or_else(2) # Just(1)
NONE |> or_else(2) # 2Use cata to execute either left function when the value is nothing or
the right function when the value is something useful.
1 |> cata(() -> 0, x -> x + 1) # 2
NONE |> cata(() -> 0, x -> x + 1) # 0It is possible to extend to your own Just and None types by implementing the
MaybeTypeTrait. Note that Nothing is given a IsNone trait by default.
The Either type is used to capture either a left or right object. To create an
Either object, simply use the left or right function. Use left_value
orright_value to extract the wrapped value. Use is_left or is_right to
check if an object is left or right. There is no discrimination which way is
better.
A special case of Either is Result, which is used for exception handling.
Use the result constructor to create a Result object. By default, any
subtypes of ErrorException are considered left. Everything else is considered
right.
julia> result(1)
MonadResult_Value(1)
julia> result(ArgumentError("bad input"))
MonadResult_Error(ArgumentError("bad input"))The convenient is_left and is_right functions can be used to
check if the object is left or right. To extract value from the
object, use left_value or right_value.
julia> is_right(result(1))
true
julia> is_left(result(ArgumentError("bad input")))
true
julia> right_value(result(1))
1
julia> left_value(result(1))
ERROR: MethodError: no method matching left_value(::Either{:R,:Result})A List monad is essentially a 1-dimensional array. Use the list constructor to create a new list monad. We can fmap over all elements, or flatten a nested list.
julia> m = list(1)
1-element Array{Int64,1}:
1
julia> v = list([1,2,3])
3-element Array{Int64,1}:
1
2
3
julia> fmap(x -> 2x, v)
3-element Array{Int64,1}:
2
4
6
julia> flatten([1, [2,3], [[4],[5]]])
5-element Array{Int64,1}:
1
2
3
4
5Maybe is a monad that either contains something useful or nothing. How is it
useful? Sometimes certain functions returns nothing rather than throwing
exception to indicate a negative condition For example:
match(r"^a.*", "hello") # nothingIt is a bit unfortunate that we must test the condition before using the result:
matched = match(r"^a.*", "hello")
result = if matched !== nothing
matched.match * " world"
else
nothing
endIf we have the notion of Maybe, then we can do it in a functional style:
"hello" |> match(r"^a.*") |> matched |> concat(" world")To make that happen, we can do the following to create composable functions that only take single arguments.
Base.match(re::Regex) = Base.Fix1(match, re)
matched(rm::RegexMatch) = rm.match
concat(s::String) = Base.Fix2(string, s)If you don't like type piracy then define your own match function or convince
the Julia core developers that it is a good addition to the Base library. And,
this would work just fine:
julia> "hello" |> match(r"^h.*") |> matched |> concat(" world")
"hello world"That's close but this doesn't work for the nothing condition.
julia> "abc" |> match(r"^h.*") |> matched |> concat(" world")
ERROR: MethodError: no method matching matched(::Nothing)With the help of fmap function, we can make it work:
julia> "abc" |> fmap(match(r"^h.*")) |> fmap(matched) |> fmap(concat(" world")) === nothing
trueThis is getting a little long and hard to read, so we just compose the functions:
process = fmap(
match(r"^h.*"),
matched,
concat(" world")
)
using Test
@test process("hello") == "hello world"
@test process("abc") == nothingLook ma, it is just a data flow pipeline without any conditional statement.
Either is a monad that contains data on the left side or right side. It is useful to keep track of two scenarios. For examples:
julia> going_to_party = left("I am sick")
MonadEither_Left(I am sick)
julia> is_left(going_to_party)
true
julia> play_badminton = right("this weekend")
MonadEither_Right(this weekend)
julia> is_right(play_badminton)
true
julia> right_value(play_badminton)
"this weekend"Result is a monad that is a special case of Either. By convention, we stay
on the right track for normal conditions but switch to the left track when we
encounter an error condition. Once we're on the left track, we stay on the it
and ignore all computation until the end. As the error condition was captured
when we switch to the left track, we can tell what went wrong when we come out
of the computation. As you can see, Either monad is useful in handling errors.
A simple example is to run a database query. As part of the process, we need to establish a connection, obtain a database cursor, and then run the query. The trouble is that it may throw an exception at any of the database api calls:
try
conn = get_connection(url)
cursor = get_cursor(conn)
sql = "select * from somehwere"
return query(cursor, sql)
catch ex
@error "Unable to run query due to ex=$ex"
rethrow(ex)
endIt would be nice if the error just flows to the end. Without using try-catch statement, we would like to do this:
# anonymous function to make it composable
run_query(sql) = cursor -> query(cursor, sql)
# error handler
handle_query_result(err::LeftEither) = @error(left_value(err))
# result set handler
handle_query_result(rs::DataFrame) = "good job"
# establish pipeline
result = fmap(
url,
get_connection,
get_cursor,
run_query("select * from sometable"),
handle_query_result
)The returned result from run_query is either a good value or an error. We can
find out if it's good or bad by calling is_right and is_left respectively.
If needed, we can also dispatch based upon ResultEither or ErrorEither
types.