Operators and functions

How come these work

==(2).(1:5)
≠(2).(1:5)
>(2).(1:5)

and these don’t ?

+(2).(1:5)
*(2).(1:5)
^(2).(1:5)

The syntax func.(collection) means applying func to the collection. For example

julia> (x -> 2x).(1:5)
5-element Vector{Int64}:
  2
  4
  6
  8
 10

In your example, ==(2), ≠(2) and >(2) are functions whereas +(2), *(2) and ^(2) are not.

julia> ==(2) isa Function
true

julia> +(2) isa Function
false

For more information, ==(2) is called a partial function application. This is a term from functional programming which means to fix an argument to a function. Specifically, ==(2) is equivalent to defining ==(x) = y -> x == y which is equivalent to Base.Fix2(==, 2). See the documentation of Base.Fix2 for more information about that function.

For readers who wonder why partial function applications are useful, they are mostly useful in combination with higher order functions, that is, functions which take functions such as map or filter. For example, compare

julia> map(x -> x < 2, 1:5)
5-element Vector{Bool}:
 1
 0
 0
 0
 0

julia> filter(x -> x == 2, 1:5)
1-element Vector{Int64}:
 2

julia> filter(s -> contains(s, "ar"), ["foo", "bar"])
1-element Vector{String}:
 "bar"

to

julia> map(<(2), 1:5)
5-element Vector{Bool}:
 1
 0
 0
 0
 0

julia> filter(==(2), 1:5)
1-element Vector{Int64}:
 2

julia> filter(contains("ar"), ["foo", "bar"])
1-element Vector{String}:
 "bar"
3 Likes

Can this notation be used to abbreviate this?

filter( x-> any( contains.(x,["a","b"]) ), ["aa","bb","cc","dd"] )

Not that I know of. Sounds like a nice idea though to allow applying AND and OR to functions. What does work here is to move the OR (any) inside the contains:

julia> filter(contains(r"a|b"), ["aa", "bb", "cc", "dd"])
2-element Vector{String}:
 "aa"
 "bb"
2 Likes

Could this functional form be available for more functions ?
2 examples

It would be nice to write this
@pipe "A=1,B=2,C=3" |> split(_,',') |> split.(_,'=')

As
"A=1,B=2,C=3" |> split(',') .|> split('=')


And it would be nice to write this

conv(x) = 
    try Date( x, dateformat"dduuuyyyy")     catch        
    try Currency_Dict[x]                    catch
    try Buy_Sell_Dict[x]                    catch
    try parse(Float64,x)                    catch    
    try Symbol(x)                           catch     
    end end end end
end

as

conv(x) = x |>
    try Date(dateformat"dduuuyyyy")     catch        
    try Currency_Dict                   catch
    try Buy_Sell_Dict                   catch
    try parse(Float64)                  catch    
    try Symbol                          catch     
    end end end end
end

The challenging part about this is that this is the following currently return something when the arguments are strings.

julia> split(",")
1-element Vector{SubString{String}}:
 ","

julia> split("=")
1-element Vector{SubString{String}}:
 "="

Nonetheless, you correctly identified that split(::Char) is not currently defined. Thus we could do:

julia> Base.split(c::AbstractChar) = Base.Fix2(Base.split, c)

julia> split(',')
(::Base.Fix2{typeof(split), Char}) (generic function with 1 method)

julia> "A=1,B=2,C=3" |> split(',') .|> split('=')
3-element Vector{Vector{SubString{String}}}:
 ["A", "1"]
 ["B", "2"]
 ["C", "3"]

help?> Base.Fix2
  Fix2(f, x)

  A type representing a partially-applied version of the two-argument function f, with the second argument fixed to
  the value "x". In other words, Fix2(f, x) behaves similarly to y->f(y, x).

A better definition would also allow for the other keyword arguments:

Base.split(c::AbstractChar; kwargs...) = Base.Fix2(Base.split, dlm; kwargs...)

The above could be potentially confusing, so I would prefer the following:

julia> Base.split(; dlm=isspace) = Base.Fix2(Base.split, dlm)

julia> "A=1,B=2,C=3" |> split(dlm=',') .|> split(dlm='=')
3-element Vector{Vector{SubString{String}}}:
 ["A", "1"]
 ["B", "2"]
 ["C", "3"]

The above does not allow for the other keywords. A keyword compatible version would look like this.

julia> Base.split(; dlm=isspace, kwargs...) = x->Base.split(x, dlm; kwargs...)

julia> "a b c" |> split()
3-element Vector{SubString{String}}:
 "a"
 "b"
 "c"

julia> "a b c" |> split(limit=2)
2-element Vector{SubString{String}}:
 "a"
 "b c"

Hot patching Base like this is frowned upon. It might be fine in a terminal project, but can create issues for composability. Another approach is to create your own split in a module or package and then import that into your namespace.

julia> module SplitFix2
           split(c::AbstractChar) = Base.Fix2(Base.split, c)
           split(; dlm=isspace, kwargs...) = x->Base.split(x, dlm; kwargs...)
       end
Main.SplitFix2

julia> import .SplitFix2: split

julia> "a b c" |> split(limit=2)
2-element Vector{SubString{String}}:
 "a"
 "b c"

julia> "A=1,B=2,C=3" |> split(dlm=',')
3-element Vector{SubString{String}}:
 "A=1"
 "B=2"
 "C=3"

julia> Base.split == SplitFix2.split
false

julia> @which split
Main.SplitFix2
1 Like

Thanks Mark.

Most functions have one argument that is the data input, and so the one I’d like to be passable from a pipe or chain. Maybe I’ll create my own versions with slightly different names (to avoid the issue you mention with split( ::string ) already being defined ).

splt(dlm)       = x -> (split(x,dlm))
fltr(f)         = x -> filter( f, x )
Dat(format)     = x -> Date( x, format)
pars(typ)       = x -> parse(typ,x)
CSV_write(file) = x -> CSV.write( file, x )

I like your version with split(dlm=',') as you’re able to keep the same name.