Stream: helpdesk (published)

Topic: Syntactic sugar for defining multiple parametric functions


view this post on Zulip Jesper Stemann Andersen (Nov 08 2021 at 13:19):

Hi,

The following is not possible:

let T <: Real
       foo(t::T) = t + t
       bar(t::T) = t - t
       baz(t::T) = t * t
end

Is there a way to avoid writing the same type parameter restrictions again and again for each function?

view this post on Zulip Fredrik Ekre (Nov 08 2021 at 13:58):

julia> let X = T where T <: Real
           global f(x::X) = x
           global g(x::X) = x
       end
g (generic function with 1 method)

julia> f(1)
1

julia> g(2.0)
2.0

view this post on Zulip Jesper Stemann Andersen (Nov 08 2021 at 14:16):

Thanks! :-)

view this post on Zulip Philipp Gabler (Nov 08 2021 at 14:18):

Fredrik Ekre said:

julia> let X = T where T <: Real

Which in this case is equivalent to let X = Real.

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 08:58):

So... how to define blocks with sets of dependent parametric types - e.g. (T1, T2) where {T1 <: Number,T2 <: AbstractVector{T1}}?

julia> let T = T1 where {T1 <: Number}, TVector = T2 where {T2 <: AbstractVector{T}}
       global function foo(x::T)
           @show typeof(x)
       end
       global function foo(v::TVector)
           @show typeof(v)
       end
       end
       foo(2)
       foo(2.0)
       foo(vec([1 2 3]))
typeof(x) = Int64
typeof(x) = Float64
ERROR: MethodError: no method matching foo(::Vector{Int64})
Closest candidates are:
  foo(::Number) at REPL[36]:3
  foo(::AbstractVector{Number}) at REPL[36]:6

Letting TVector = AbstractVector (without the parameterization on T), and defining foo(v::TVector{T}) also does not define foo for ::Vector{Int64}.

view this post on Zulip Fredrik Ekre (Nov 22 2021 at 09:01):

Note that

julia> Vector{Int} <: Vector{Real}
false

julia> Vector{Int} <: Vector{<:Real}
true

see https://docs.julialang.org/en/v1/manual/types/#man-parametric-composite-types

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 09:07):

Let me answer myself...

This works at least (the generic function must have a where T):

let T = Number, TVector = AbstractVector
    global function foo(x::T)
        @show typeof(x)
    end
    global function foo(v::TVector{T}) where T
        @show typeof(v)
    end
end

foo(2)
foo(2.0)
foo(vec([1 2 3]))

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 09:25):

... but all I achieved for AbstractVector/TVector was temporarily renaming it...

view this post on Zulip Sukera (Nov 22 2021 at 09:27):

parametric types in julia are invariant (except tuples)

view this post on Zulip Sukera (Nov 22 2021 at 09:27):

what would you like to achieve?

view this post on Zulip Sukera (Nov 22 2021 at 09:28):

if you want to define a function that takes vectors with elements of any number type, you can define foo(v::AbstractVector{<: Number})

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 09:30):

I would just like to avoid writing the same where {T1 <: AbstractT1, T2 <: AbstractT2{T1}} over and over for a block of methods.

view this post on Zulip Sukera (Nov 22 2021 at 09:31):

do you actually require T2 in the method?

view this post on Zulip Sukera (Nov 22 2021 at 09:31):

if not, the way I've written it above is equvialent

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 09:36):

Yes, I think so - see for a very simple example the two functions for SerializationTypeSerializer: https://github.com/IHPSystems/TypeSerializers.jl/blob/main/src/TypeSerializers.jl

view this post on Zulip Sukera (Nov 22 2021 at 09:37):

those don't use TSerializer in the method itself though, only T

view this post on Zulip Sukera (Nov 22 2021 at 09:37):

in which case foo(s::SerializationTypeSerializer{T})::T where T should be the same

view this post on Zulip Sukera (Nov 22 2021 at 09:39):

or more concrete for your example ::Type{<:SerializationTypeSerializer{T}}, I think

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 09:39):

This is what got me started (using those "type serializers"):
https://github.com/IHPSystems/SerializingChannels.jl/blob/main/src/SerializingChannels.jl

view this post on Zulip Sukera (Nov 22 2021 at 09:40):

(though intuitively, to me that already reads like relying a lot of type hierarchies, complicating things)

view this post on Zulip Sukera (Nov 22 2021 at 09:41):

if you're already restricting the type at construction, why check for the same requirement on method definition as well?

view this post on Zulip Sukera (Nov 22 2021 at 09:43):

e.g., the struct and the first function should be equivalent to this

struct SerializingChannel{T, TChannel <: AbstractChannel{Vector{UInt8}}, <: AbstractTypeSerializer{T}}
    channel::TChannel
end

function Base.put!(channel::SerializingChannel{T, _, TSerializer}, value::T) where {T, TSerializer}
    stream = serialize(TSerializer, value)
    data = take!(stream)
    put!(channel.channel, data)
end

if I'm not mistaken

view this post on Zulip Sukera (Nov 22 2021 at 09:43):

you don't have to give things a name if you're not going to use them in the function

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 09:47):

Ah yes - I see. Thanks a lot!

view this post on Zulip Sukera (Nov 22 2021 at 09:50):

you're welcome!

view this post on Zulip Sukera (Nov 22 2021 at 09:51):

also, if you want to avoid allocations, you may want to pass in an IO object instead of constructing one in serialize, only for it to be destroyed immediately again :)

view this post on Zulip Sukera (Nov 22 2021 at 09:51):

though that may be harder to work into your current design, since you have a channel of Vector{UInt8}

view this post on Zulip Sukera (Nov 22 2021 at 09:51):

(I'm also only speculating here, so may not be applicable to your situation)

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 10:05):

But I guess the programmer is still left to repetitive type constraints in the general case? I.e. where the type parameterization include abstract parametric types - like in the Number, AbstractVector{<:Number} example above.

view this post on Zulip Sukera (Nov 22 2021 at 10:07):

you mean across methods?

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 10:08):

Sukera said:

also, if you want to avoid allocations, you may want to pass in an IO object instead of constructing one in serialize, only for it to be destroyed immediately again :)

Yeah - might be possible. It's highly targeted on being able to use FlatBuffers for serialization and Zmq as a transport.

It was also a bit of a struggle to make it play nice with the Serialization module...

view this post on Zulip Sukera (Nov 22 2021 at 10:09):

julia> const T = Number
Number

julia> const TVec = AbstractVector{<:T}
AbstractVector{<:Number} (alias for AbstractArray{<:Number, 1})

julia> foo(v::TVec) = "nope"
foo (generic function with 1 method)

julia> foo(Int[])
"nope"

view this post on Zulip Sukera (Nov 22 2021 at 10:09):

though that only works when you don't have interdependencies between two types in a signature

view this post on Zulip Sukera (Nov 22 2021 at 10:10):

so if you require one type parameter of one type and another of another type to be the same, you _will_ need that where

view this post on Zulip Sukera (Nov 22 2021 at 10:10):

there's no way to express two different types having the same "slot" without specifying in which method that should apply

view this post on Zulip Sukera (Nov 22 2021 at 10:11):

(i.e. there's no real dependent typing in julia)

view this post on Zulip Sukera (Nov 22 2021 at 10:12):

also note that those const T are really only constant aliases - they're _not_ their own "versions" of that type

view this post on Zulip Sukera (Nov 22 2021 at 10:13):

It's highly targeted on being able to use FlatBuffers for serialization

well right now our serialize is always allocating an IOBuffer with its corresponding internal Vector{UInt8}, right?

view this post on Zulip Sukera (Nov 22 2021 at 10:13):

so my guess would be that it would be more efficient to directly write to the FlatBuffer instead of having those intermediary stores

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 10:19):

Sukera said:

julia> const T = Number
Number

julia> const TVec = AbstractVector{<:T}
AbstractVector{<:Number} (alias for AbstractArray{<:Number, 1})

julia> foo(v::TVec) = "nope"
foo (generic function with 1 method)

julia> foo(Int[])
"nope"

Excellent. A bit puzzled though, that I did not manage to make those T, TVec definitions work in a let block...
Should they just be nested let blocks? And how then to global'ize the method definitions?

view this post on Zulip Fredrik Ekre (Nov 22 2021 at 10:21):

Why doesn't it work with let?

julia> let T = Number, TVec = AbstractVector{<:T}
           global foo(::TVec) = "nope"
       end
foo (generic function with 1 method)

julia> foo(Int[])
"nope"

view this post on Zulip Jesper Stemann Andersen (Nov 22 2021 at 10:27):

Ah - it was probably just the parametric type invariance that I messed up (defining TVec = AbstractVector{T} instead of TVec = AbstractVector{<:T}).

Thanks again :-)


Last updated: Dec 28 2024 at 04:38 UTC