MLStyle.jl icon indicating copy to clipboard operation
MLStyle.jl copied to clipboard

slurp Tuples

Open MasonProtter opened this issue 5 years ago • 7 comments

It's be good if MLStyle.jl could support slurping tuples like it does with arrays:

julia> using MLStyle

julia> @match [1, 2, 3, 4] [a, b..., c] => (a, b, c) 
(1, [2, 3], 4)

julia> @match (1, 2, 3, 4) (a, b..., c) => (a, b, c)
ERROR: LoadError: PatternCompilationError(:(#= REPL[3]:1 =#), "unknown pattern syntax :(b...)")
Stacktrace:
 [1] gen_match(::Expr, ::Expr, ::LineNumberNode, ::Module) at /home/mason/.julia/dev/MLStyle/src/MatchImpl.jl:474
 [2] @match(::LineNumberNode, ::Module, ::Any, ::Any) at /home/mason/.julia/dev/MLStyle/src/MatchImpl.jl:454
in expression starting at REPL[3]:1
caused by [exception 1]
unknown pattern syntax :(b...)

It'd be nice if the final line evalutated to (1, (2, 3), 4).

MasonProtter avatar Aug 19 '20 20:08 MasonProtter

Okay, considering this feature.

It's not difficult to add, but I hope to see some use cases.

Tuples are after all heterogeneous data, if you want to dynamically slice a tuple, I guess you might need to use a vector there?

thautwarm avatar Aug 20 '20 10:08 thautwarm

Tuples are after all heterogeneous data, if you want to dynamically slice a tuple, I guess you might need to use a vector there?

It's not necessarily dynamic if you know the size of the Tuple, which is a part of it's type. If you write e.g.

f(t::Tuple) = @match t (a, b..., c) => (a, b, c)

the sizes can be statically known. I'm sure there's a tuple-recursion version, but here's a type inferrable @generated function that does it:

julia> @generated function f(t::NTuple{N}) where {N}
           Expr(:tuple, :(t[1]), Expr(:tuple, (:(t[$i]) for i ∈ 2:N-1)...), :(t[$N]))
       end
f (generic function with 2 methods)

julia> f((1,2,3,4,5,6,7,8))
(1, (2, 3, 4, 5, 6, 7), 8)

julia> @code_typed f((1,2,3,4,5,6,7,8))
CodeInfo(
1 ─ %1  = Base.getfield(t, 1, true)::Int64
│   %2  = Base.getfield(t, 2, true)::Int64
│   %3  = Base.getfield(t, 3, true)::Int64
│   %4  = Base.getfield(t, 4, true)::Int64
│   %5  = Base.getfield(t, 5, true)::Int64
│   %6  = Base.getfield(t, 6, true)::Int64
│   %7  = Base.getfield(t, 7, true)::Int64
│   %8  = Core.tuple(%2, %3, %4, %5, %6, %7)::NTuple{6,Int64}
│   %9  = Base.getfield(t, 8, true)::Int64
│   %10 = Core.tuple(%1, %8, %9)::Tuple{Int64,NTuple{6,Int64},Int64}
└──       return %10
) => Tuple{Int64,NTuple{6,Int64},Int64}

This sort of thing is generally useful when you're dealing with functions that return tuples. Often in julia people write things like

julia> t = (1,2,3)
(1, 2, 3)

julia> a, b = t
(1, 2, 3)

julia> a
1

julia> b
2

but that's not very nice because in the destructuring step a, b = t, there's no way to know if you're throwing away data or not. Having an ability to slurp here would be quite nice because then you don't lose anything in the destructuring.

MasonProtter avatar Aug 20 '20 18:08 MasonProtter

Yes, using generated functions can enable many advanced things without a performance cost, however it seems impossible for MLStyle to generate generated functions...

I hope the generated code from MLStyle macros will not depend on MLStyle itself, so if I generate generated functions, I have to generate it inside the macrocall wrapped block, which is always not at the top level and might be in the body of nested functions, where generated functions are invalid.

I feel it better to add another library to extend MLStyle with tuple slurping, where we can define some generated functions as runtime deps.

thautwarm avatar Aug 22 '20 01:08 thautwarm

Yes, using generated functions can enable many advanced things without a performance cost, however it seems impossible for MLStyle to generate generated functions...

Ah I see. In that case, here's a non-generated function that can do it in a type stable manner for arbitrarily large Tuples

julia> function f(t::NTuple{N, Any}) where {N}
           first(t), ntuple(i -> (j = i + 1; t[j]), Val(N-2)), last(t)
       end
f (generic function with 1 method)

julia> f((1, 2.0, 3f0, "four", :five, Int32(6), Int16(7), Int8(8)))
(1, (2.0, 3.0f0, "four", :five, 6, 7), 8)

julia> @code_typed f((1, 2.0, 3f0, "four", :five, Int32(6), Int16(7), Int8(8)))
CodeInfo(
1 ─ %1  = Base.getfield(t, 1, true)::Int64
│   %2  = Base.getfield(t, 2, true)::Float64
│   %3  = Base.getfield(t, 3, true)::Float32
│   %4  = Base.getfield(t, 4, true)::String
│   %5  = Base.getfield(t, 5, true)::Symbol
│   %6  = Base.getfield(t, 6, true)::Int32
│   %7  = Base.getfield(t, 7, true)::Int16
│   %8  = Base.tuple(%2, %3, %4, %5, %6, %7)::Tuple{Float64,Float32,String,Symbol,Int32,Int16}
│   %9  = Base.getfield(t, 8, true)::Int8
│   %10 = Core.tuple(%1, %8, %9)::Tuple{Int64,Tuple{Float64,Float32,String,Symbol,Int32,Int16},Int8}
└──       return %10
) => Tuple{Int64,Tuple{Float64,Float32,String,Symbol,Int32,Int16},Int8}

julia> Core.Compiler.return_type(f, Tuple{NTuple{100, Int}})
Tuple{Int64,NTuple{98,Int64},Int64}

julia> let t = ntuple(_ -> rand([1, "hi", 2.0, :bye, [true, false]]), 100)
           Core.Compiler.return_type(f, Tuple{typeof(t)})
       end
Tuple{Symbol,Tuple{Int64,String,Float64,String,Array{Bool,1},Int64,Float64,Symbol,Int64,Array{Bool,1},Float64,Symbol,String,Int64,Int64,Symbol,Float64,Symbol,String,Symbol,Array{Bool,1},Symbol,Symbol,Array{Bool,1},String,Array{Bool,1},Array{Bool,1},Float64,Int64,Array{Bool,1},Int64,Array{Bool,1},Float64,String,Float64,Int64,Symbol,Symbol,String,Symbol,Float64,Float64,Array{Bool,1},Symbol,Float64,Symbol,Array{Bool,1},Float64,String,Symbol,Float64,String,Symbol,Symbol,Array{Bool,1},Float64,String,Int64,Int64,Int64,Int64,Array{Bool,1},Float64,Array{Bool,1},String,Array{Bool,1},Float64,Float64,Int64,Float64,String,Symbol,Float64,Int64,String,Symbol,Int64,Symbol,String,Float64,Symbol,Float64,Int64,Int64,Symbol,Symbol,Array{Bool,1},String,Symbol,String,String,Symbol,Array{Bool,1},Array{Bool,1},Array{Bool,1},Array{Bool,1},Int64,Float64},Symbol}

MasonProtter avatar Aug 22 '20 01:08 MasonProtter

This is really impressive. I think I can do this in MLStyle. I'll implement it before the next patch release.

thautwarm avatar Aug 22 '20 01:08 thautwarm

Some hints if you want to implement it before I make actions. Feel free to ask me anything on slack if you have questions/problems..

https://github.com/thautwarm/MLStyle.jl/blob/8bba7028879a2f37774af1d919859dfb4bda83a5/src/MatchImpl.jl#L196

tuple pattern: https://github.com/thautwarm/MLStyle.jl/blob/8bba7028879a2f37774af1d919859dfb4bda83a5/src/AbstractPatterns/impl/BasicPatterns.jl#L102

vector slurping pattern: https://github.com/thautwarm/MLStyle.jl/blob/8bba7028879a2f37774af1d919859dfb4bda83a5/src/AbstractPatterns/impl/BasicPatterns.jl#L150

thautwarm avatar Aug 22 '20 01:08 thautwarm

Fantastic, I'll look into it if I have time in the near future!

MasonProtter avatar Aug 22 '20 02:08 MasonProtter