JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.55k stars 5.47k forks source link

proposal: map(f, ::Some{T}) #33701

Open reganbaucke opened 4 years ago

reganbaucke commented 4 years ago

Base currently supports a basic implementation of the Option type. As it is currently implemented, Base defines a Some as

struct Some{T}
    value::T
end

This is all that is necessary for the Some type, but other facilities are lacking. Within julia/base/some.jl, it is mentioned that this is for use in the type union defined as Union{Some{T},Nothing}. I propose that this type union should be given a type alias: const Option{T} = Union{Some{T},Nothing}.

Options are useful in themselves, but a lot of their power comes from their ability to be 'mapped' into. This is not currently implemented in Julia. To finally give this feature its minimal completion, I propose that the map function ought to have the following implementation on these types:

function map(f::Function, option::Some{T}) where T
    Some(f(option.value))
end

function map(f::Function, option::Nothing) 
    nothing
end
StefanKarpinski commented 4 years ago

map is for collections; Some isn't really a collection, so this seems like an odd interface.

reganbaucke commented 4 years ago

One might argue that map is a function for transforming a 'normal' function to a function 'with a context'. For instance:

add_one(x) = x + 1
contextual_add_one(x) = map(add_one,x)

contextual_add_one([2,4]) == [3, 5]       #true
contextual_add_one(Some(5)) == Some(6)    #true

Of course in Julia, our usual 'context' is values contained within collections such as arrays or sets, but these need not necessarily be the only cases.

StefanKarpinski commented 4 years ago

Based on what precedent or principle? It seems like a departure from what map means normally.

vtjnash commented 4 years ago

Many languages do pun on this (e.g. C# linq, swift)

rfourquet commented 4 years ago

This principle is taken seriously in Haskell (i.e. they don't take it as a pun), "functor" is a "type class" on which fmap can be applied. Collections, the Maybe type, Either and many other types are instances of this type class. To be a "functor" type, fmap must respect two simple laws (in Julia syntax): fmap(identity, x) == x (when x is a functor), and composition: fmap(f∘g, x) == fmap(f, fmap(g, x)), or in other words, fmap(f∘g, x) == (fmap(f)∘fmap(g))(x), where fmap(f) = x -> fmap(f, x).

The notion of "value with context" is often used in tutorials on functors or monads, but not all Haskell experts agree that this is a good way to explain it. Basically, a functor represent a value with context, and fmap applies a function to this value, the implementation keeps track of the context. A collection is functor (you get the value by indexing), a function is a functor (you get the value by calling the function), Union{Some{T},Nothing} is a functor ("you may have a value"), etc.

reganbaucke commented 4 years ago

Although there are many different interpretations, thinking about map in this way is based upon a mathematical principle from category theory. It does not in any way preclude/impact how map might be understood in Julia as performing 'element-wise' operations on a collection.

This concept has been found to be useful in languages that support a 'functional' style such as Haskell, OCaml, Scala etc.

Basically, if a 'parameterized' type has an implementation of map that obeys certain laws, then that parameterized type is called a 'functor'. From this point, many useful programming abstractions can be made. The implementation of map for Option shown above is the standard one from these aforementioned languages.

vchuravy commented 4 years ago

x-ref: #9446 and especially https://github.com/JuliaLang/julia/pull/9446#issuecomment-68502187 and http://joeduffyblog.com/2016/02/07/the-error-model/#non-null-types which was referenced later.

tkf commented 4 years ago

I suppose going functor route would require coordination with the map API on Dict #5794?

nalimilan commented 4 years ago

If we supported f?(x) to propagate nothing/missing, it would make sense to have it return Some(f(x.value)) for Some arguments instead of using map(f, x) (which is indeed a common approach in many languages which treat nullables as containers).

tk3369 commented 4 years ago

I'm experimenting some of these concepts while learning how to work with monads. Take a look at https://github.com/tk3369/MonadFunctions.jl

schlichtanders commented 11 months ago

I build an entire package to have clean monad-like julia types which interact nicely https://github.com/JuliaFunctional/DataTypesBasic.jl

the monadic interface is defined in https://github.com/JuliaFunctional/TypeClasses.jl

My learning back then is that map is the best function to be used for functor map (e.g. SparseVector behaves functorial and is not creating a normal dense Vector). However instead of Some{T} and Nothing, which has a special meaning in Julia, which is not necessarily well-suited for monadic code, the DataTypesBasic introduce Identity{T} and Const{T}.

Take a look at the packages - it so much nicer to work with in monadic style.