3 ms·
MLStyle.jl is nice, but I think Julia would really benefit from having it enshrined in the language. Multiple dispatch is great for a lot of problems, but somet
by hexane360 5y ago
MLStyle.jl is nice, but I think Julia would really benefit from having it enshrined in the language. Multiple dispatch is great for a lot of problems, but sometimes it makes more sense to do your pattern matching inline.
For tagged unions, the difference is in memory layout. A real tagged union type would eliminate indirection and allow more code to be type stable.
For instance,
mutable struct NotTypeStable
o::Union{Some{Int}, Nothing}
end
function not_type_stable()
o = returns_option()
if isnothing(o)
0
else
1
end
end
versus:
enum Option{T}
Some(T)
None
end
mutable struct TypeStable
o::Option{Int}
end
function type_stable()
o = returns_option()
match o
Some(_) => 1
None => 0
end
end
Both of these features aren't strictly necessary, but they act as a compliment to Julia's existing dynamic mechanisms (multiple dispatch & Union types)
- celrod 5y agoTry Julia nightly (1.7): julia> @code_warntype not_type_stable() MethodInstance for not_type_stable() from not_type_stable() in Main at REPL[2]:1 Arguments #self#::Core.Const(not_type_stable) Locals o::Any Body::Int64 1 ─ (o = Main.returns_option()) │ %2 = Main.isnothing(o)::Bool └── goto #3 if not %2 2 ─ return 0 3 ─ return 1 julia> versioninfo() Julia Version 1.7.0-DEV.1169 Commit e5d7ef01b0* (2021-05-26 14:17 UTC) Note `Body::Int64`. With older Julia versions, it should still be type stable if you did `o === nothing` instead of `isnothing(o)`.
- hexane360 5y agoRight, I'm referring to the local o being Any, meaning that it must be behind a pointer lookup. I should have clarified because the more common usage is about the function's return value.
- eigenspace 5y agoHere's me copying your second example as closely as possible in julia: using MLStyle mutable struct TypeStable o::Union{Some{Int}, Nothing} end MLStyle.@as_record TypeStable MLStyle.@as_record Nothing function type_stable() o = returns_option() @match o.o begin Some(_) => 1 Nothing() => 0 end end returns_option() = TypeStable(rand(Bool) ? Some(rand(1:10)) : nothing) And what does the compiler have to say about it? julia> Core.Compiler.return_type(type_stable, Tuple{}) Int64 and julia> @code_warntype type_stable() Variables #self#::Core.Const(type_stable) o::TypeStable 259::Union{Nothing, Some{Int64}} return#257::Union{Nothing, Int64} Body::Int64 1 ─ (o = Main.returns_option()) │ (return#257 = Main.nothing) │ (259 = Base.getproperty(o, :o)) │ %4 = (259 isa Nothing)::Bool └── goto #3 if not %4 2 ─ (return#257 = 0) └── goto #5 3 ─ %8 = (259::Some{Int64} isa Some)::Core.Const(true) │ %8 │ %10 = (259::Some{Int64} !== Main.nothing)::Core.Const(true) │ %10 │ (return#257 = 1) └── goto #5 4 ─ Core.Const(:((error)("matching non-exhaustive, at #= REPL[9]:3 =#"))) 5 ┄ return return#257::Int64 This is on version 1.6.1 for me, but should work fine on earlier releases. I agree that having MLStyle.jl pattern matching bundled into julia would be great!
- hexane360 5y agoDoesn't the TypeStable struct just hide the unknown type, not remove it? The type of o.o still needs to be looked up at runtime. Furthermore, TypeStable itself has to be on the heap, which I think means this causes two pointer lookups rather than just one.
- eigenspace 5y ago> Doesn't the TypeStable struct just hide the unknown type, not remove it? No, of you look at the typed IR I posted, it says 259::Union{Nothing, Some{Int64}} and (259 = Base.getproperty(o, :o)) So it's inferred properly to be a small union which julia handles efficiently at runtime: julia> @btime type_stable() 18.988 ns (1 allocation: 32 bytes) 1 > Furthermore, TypeStable itself has to be on the heap, which I think means this causes two pointer lookups rather than just one. It's on the heap because you wrote mutable struct in your example and I was just mimicking you as close as possible. If you don't actually want it to be mutable, then you can just remove the keyword mutable and it'll be even faster and have no heap allocations: julia> @btime type_stable() 18.426 ns (0 allocations: 0 bytes) 1
- hexane360 5y ago>small union which julia handles efficiently at runtime Oh! I didn't realize Julia already optimized small unions. https://julialang.org/blog/2018/08/union-splitting/ https://julialang.org/blog/2018/08/union-splitting/ https://docs.julialang.org/en/v1/devdocs/isbitsunionarrays/ https://docs.julialang.org/en/v1/devdocs/isbitsunionarrays/ I wasn't quite sure whether something like this was possible given the semantics of Julia. Personally, I'd still prefer something more explicit, so it would be more obvious when a union is being handled efficiently. But given that small unions are optimized, I can see why Julia would make the opposite decision. This makes me feel a lot better about writing Julia code with union types. Thanks
- eigenspace 5y agoYep, small union optimizations are a godsend!