Skip to content
This repository was archived by the owner on Feb 7, 2019. It is now read-only.

[WIP] first stab at parametrically-typed trait (e.g. monad) #4

Closed
wants to merge 13 commits into from
30 changes: 20 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ end
Z = promote_type(X,Y) # calculates Z from X and Y
fun5(X,Y) -> Z
end

# using parametric trait. Note the nested curly
@traitdef SemiFunctor{X{Y}} begin
fmap( Function, X{Y} } -> Any
end

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the return type not be more specific here? Otherwise there seems to be no need have a parametric type if none of the parameters actually feature.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I thought about that, but for staging to work, I think we would need access to the input/output type of functions during the staging phase. Here's a recent comment I made on this topic: JuliaLang/julia#7474 (comment)

```
Note that return-type checking is quite experimental. It can be
turned off by defining `Main.Traits_check_return_types=false` before
Expand Down Expand Up @@ -155,6 +161,17 @@ try
catch e
println(e) # ErrorException("assertion failed: istrait(Tr4{Int,Float64})")
end

# for parametric trait,
@traitimpl SemiFunctor{Nullable{T}} begin
fmap{T}( f::Function, x::Nullable{T}) = isnull(x) ? Nullable() : Nullable(f(x.value))
end
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation of fmap will make a non-null value out of a null value:

julia> Nullable(sin(Nullable{Int}().value))
Nullable(0.8414709848078965)

Is that intended?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's not. I'll fix.


# for Array, it is a bit difficult because the eltype is the first argument.
# Also note that this sample implementation won’t cover higher dimensions
@traitimpl SemiFunctor{Array{T...}} begin
fmap{T}( f::Function, x::Array{T,1}) = map(f, x)
end
```

Trait functions & dispatch:
Expand Down Expand Up @@ -342,16 +359,9 @@ do not have a strict hierarchy like types.

- Are there better ways for trait-dispatch?

- Sometimes it would be good to get at type parameters, for instance
for Arrays and the like:
```julia
@traitdef Indexable{X{Y}} begin
getindex(X, Any) -> Y
setindex!(X, Y, Any) -> X
end
```
This problem is similar to triangular dispatch and may be solved
by: https://github.com/JuliaLang/julia/issues/6984#issuecomment-49751358
- Issues related to parametric trait:
* Triangular dispatch:
https://github.com/JuliaLang/julia/issues/6984#issuecomment-49751358

# Issues

Expand Down
131 changes: 131 additions & 0 deletions examples/monads.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using Traits

immutable FuncFullSig{TI, TO}
f::Function
end

call{TI,TO}( fs::FuncFullSig{TI,TO}, x::TI ) = (fs.f(x))::TO

# I use the prefix "Semi" to remind ourselves that the
# there may not be a guarantee of function output type
@traitdef SemiFunctor{X{Y}} begin
fmap( Function, X{Y} ) -> Any
end

@traitdef Functor{X{Y}} begin
fmap( FuncFullSig{Y,Z}, X{Y} ) -> X{Z}
end

@traitdef SemiMonad{X{Y}} begin
mreturn( ::X,Y) ->X{Y}
bind( Function, X{Y} ) -> Any
end

# ::X is the shorthand for singleton type argument
@traitdef Monad{X{Y}} begin
mreturn(::X, Y) -> X{Y} # we cannot infer X so we have to supply it
bind( FuncFullSig{Y, X{Y}}, X{Y} ) -> X{Y}
end

# === implementation of traits
@traitimpl SemiMonad{ Nullable{Y} } begin
mreturn{Y}( ::Type{Nullable}, x::Y ) = Nullable{Y}(x)
bind{Y}( f::Function, x::Nullable{Y} ) = begin
if isnull(x)
return Nullable()
else
try
return f( x.value )
catch
return Nullable()
end
end
end
end

@traitimpl SemiMonad{ Array{Y...} } begin
mreturn{Y}( ::Type{Array}, x::Y ) = Y[x]
bind{Y}( f::Function, x::Array{Y,1} ) = [ f(_) for _ in x ]
end

# === some combo traits ======
@traitdef MonadRelated1{ X{Y}, Z } <: SemiMonad{X} begin
@constraints begin
Y == Z
end
end

@traitimpl SemiFunctor{ Array{Y...} } begin
fmap{Y}( f::Function, x::Array{Y,1} ) = map(f, x)
end

# the deparameterize_type is ugly, but at least we don't have
# to do it many times
@traitfn mequal{M,Y;MonadRelated1{M,Y}}( x::M, y::Y ) =
isequal( x, mreturn( Traits.deparameterize_type(M), y ) )

@traitfn mequal{Y,M;MonadRelated1{M,Y}}( x::Y, y::M ) =
isequal( y, mreturn( Traits.deparameterize_type(M), x ) )

mequal( x, y ) = isequal( x, y )

@assert mequal( Nullable( 1.0 ), 1.0 )
@assert mequal( 1.0, Nullable( 1.0 ) )

# but it's better than that. Since we have Array being a SemiMonad,
# we get this for free
@assert mequal( 1.0, [ 1.0 ] )
@assert mequal( [1.0], 1.0 )

# now we compare an Array of nullables and a simple Array
@traitdef CollectionM{X{Y}} <: Collection{X} begin
@constraints begin
istrait( SemiMonad{Y} ) # we cannot put it in the header
end
end
@traitdef IterM{X{Y}} <: Iter{X} begin
@constraints begin
istrait( SemiMonad{Y} ) # we cannot put it in the header
end
end

@traitdef SemiFunctorMonad{X{Y}} <: SemiFunctor{X} begin
@constraints begin
istrait( SemiMonad{Y} )
end
end

@traitfn mequal(X,Y;CollectionM{X}, Collection{Y})( xc::X, yc::Y ) = begin
println( "using CollectionM comparison")
if length(xc) != length(yc)
return false
end
xs = start(xc)
ys = start(yc)
while( !done(xc, xs) )
x,xs = next(xc,xs)
y,ys = next(yc,ys)
if !mequal( x, y )
return false
end
end
return true
end
@traitfn mequal(X,Y;IterM{X}, Iter{Y})( xc::X, yc::Y ) = begin
println( "using IterM comparison")
xs = start(xc)
ys = start(yc)
while( !done(xc, xs) && !done( yc, ys) )
x,xs = next(xc,xs)
y,ys = next(yc,ys)
if !mequal( x, y )
return false
end
end
if !done(xc,xs) || !done(yc,ys)
return false
end
return true
end

@assert mequal( [ Nullable(1.0), Nullable(2.0) ], [1.0, 2.0 ] )
60 changes: 40 additions & 20 deletions src/Traits.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
module Traits
@doc """This package provides an implementation of traits, aka interfaces or type-classes.
@doc """This package provides an implementation of traits, aka interfaces or type-classes.
It is based on the premises that traits are:

- contracts on one type or between several types. The contract can
contain required methods but also other assertions and just
belonging to a group (i.e. the trait).
- they are structural types: i.e. they needn't be declared explicitly
""" -> current_module()

export istrait, istraittype, issubtrait,
traitgetsuper, traitgetpara, traitmethods,
traitgetsuper, traitgetpara, traitmethods,
@traitdef, @traitimpl, @traitfn, TraitException, All

if !(VERSION>v"0.4-")
Expand All @@ -32,9 +32,9 @@ end
SUPER of Trait is needed to specify super-traits (a tuple).""" ->
abstract Trait{SUPER}

# A concrete trait type has the form
# A concrete trait type has the form
## Tr{X,Y,Z} <: Trait{(ST1{X,Y},ST2{Z})}
#
#
# immutable Tr{X,Y,Z} <: Trait{(ST1{X,Y},ST2{Z})}
# methods
# Tr() = new(methods_made_in_macro)
Expand All @@ -52,16 +52,16 @@ immutable _TraitStorage end

@doc """Type All is to denote that any type goes in type signatures in
@traitdef. This is a bit awkward:

- method_exists(f, s) returns true if there is a method of f with
signature sig such that s<:sig. Thus All<->Union()
- Base.return_types works the other way around, there All<->Any

See also https://github.com/JuliaLang/julia/issues/8974"""->
abstract All

# General trait exception
type TraitException <: Exception
type TraitException <: Exception
msg::String
end

Expand Down Expand Up @@ -92,30 +92,48 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false)
# check supertraits
!istrait(traitgetsuper(Tr); verbose=verbose) && return false
# check methods definitions
try
Tr()
local tr::T
try
tr=Tr()
catch
if verbose
println("""Not all generic functions of trait $Tr are defined.
println("""Not all generic functions of trait $Tr are defined.
Define them before using $Tr""")
end
return false
end
out = true
function testanytypevars( at )
if typeof(at) == TypeVar
return true
elseif typeof(at) == DataType
return( any( y->typeof(y) == TypeVar, at.parameters ) )
elseif typeof(at) <: Tuple
for sat in at
if testanytypevars(sat)
return true
end
end
return false
else
println( "unknown type in signature " * string( typeof( at ) ) * " val: " * string(at) )
end
end
anytypevars = false
# check call signature of methods:
for (meth,sig) in Tr().methods
for (meth,sig) in tr.methods
# instead of:
## checks = length(methods(meth, sig[1]))>0
# Now using method_exists. But see bug
# https://github.com/JuliaLang/julia/issues/8959

sigg = map(x->x===All ? Union() : x, sig[1])
anytypevars = testanytypevars( sig[1] )

if isa(meth, Function)
if !method_exists(meth, sigg) # I think this does the right thing.
if verbose
println("Method $meth with call signature $(sig[1]) not defined for $T")
end
out = false
out = !anytypevars ? method_exists(meth,sigg) : method_exists_tvars( meth,sigg,verbose )
if !out && verbose
println("Method $meth with call signature $(sig[1]) not defined for $T")
end
elseif isa(meth, DataType) # a constructor, presumably.
# But discard the catch all to convert, i.e. this means
Expand All @@ -132,8 +150,10 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false)
end
end
# check return-type
if flag_check_return_types && out # only check if all methods were defined
for (meth,sig) in Tr().methods
# unfortunately if the sig has TypeVar in them it doesn't seem possible to check
# the return type
if !anytypevars && flag_check_return_types && out # only check if all methods were defined
for (meth,sig) in tr.methods
# replace All in sig[1] with Any
sigg = map(x->x===All ? Any : x, sig[1])
tmp = Base.return_types(meth, sigg)
Expand All @@ -158,7 +178,7 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false)
end
end
# check constraints
if !all(Tr().constraints)
if !all(tr.constraints)
if verbose
println("Not all constraints are satisfied for $T")
end
Expand Down
Loading