As others suggested, new users typically get used to the functional style of Julia pretty quickly. In some cases, e.g. when experimenting in REPL, I also use |>
operator to chain the calls, e.g.:
x = 2.0
x |> sin |> cos |> tanh
It’s convenient because you don’t need to move the cursor to the beginning of the line and count opening and closing parentheses. Yet, for multiarg functions it’s pretty verbose:
x |> sin |> cos |> x -> x + 1
This syntax is also less readable, so I rarely use it in a package code, preferring normal call notation:
x = sin(x)
x = cos(x)
x = x + 1
So in 99% of cases it makes code better and more idiomatic. But in some rare cases you really, really want the OO-style dot syntax for function chaining. I faced such a case during work on Spark.jl, which mimics PySpark API. PySpark uses function chaining a lot! For example, creating a Spark session looks like this:
spark = SparkSession.builder.appName("Main").master("local").getOrCreate()
Splitting it into 4 lines of code (+1 line for every custom config) would look cumbersome! Also, since we mimic a Python library, we want an easy way to translate thousands of Python examples into Julia.
To support this (again, pretty specific) case, I wrote a simple macro @chainable
which overloads struct’s getproperty()
method in such a way that calling obj.f(args...)
is translated into f(obj, args...)
. For example:
julia> struct Point x; y end
julia> plus_x(p1::Point, p2::Point) = Point(p1.x + p2.x, p1.y)
plus_x (generic function with 1 method)
julia> mul_y(p::Point, val::Real) = Point(p.x, p.y * val)
mul_y (generic function with 1 method)
julia> p1 = Point(1.0, 2.0)
Point(1.0, 2.0)
julia> p2 = Point(3.0, 4.0)
Point(3.0, 4.0)
julia> p1.plus_x(p2).mul_y(5)
Point(4.0, 10.0)
`@chainable` implementation
"""
DotChainer{O, Fn}
See `@chainable` for details.
"""
struct DotChainer{O, Fn}
obj::O
fn::Fn
end
# DotChainer(obj, fn) = DotChainer{typeof(obj), typeof(fn)}(obj, fn)
(c::DotChainer)(args...) = c.fn(c.obj, args...)
"""
@chainable T
Adds dot chaining syntax to the type, i.e. automatically translate:
foo.bar(a)
into
bar(foo, a)
For single-argument functions also support implicit calls, e.g:
foo.bar.baz(a, b)
is treated the same as:
foo.bar().baz(a, b)
Note that `@chainable` works by overloading `Base.getproperty()`,
making it impossible to customize it for `T`. To have more control,
one may use the underlying wrapper type - `DotCaller`.
"""
macro chainable(T)
return quote
function Base.getproperty(obj::$(esc(T)), prop::Symbol)
if hasfield(typeof(obj), prop)
return getfield(obj, prop)
elseif isdefined(@__MODULE__, prop)
fn = getfield(@__MODULE__, prop)
return DotChainer(obj, fn)
else
error("type $(typeof(obj)) has no field $prop")
end
end
end
end
function Base.getproperty(dc::DotChainer, prop::Symbol)
if hasfield(typeof(dc), prop)
return getfield(dc, prop)
else
# implicitely call function without arguments
# and propagate getproperty to the returned object
return getproperty(dc(), prop)
end
end