Julia encourages programmers to put as much code as possible inside functions that can be JIT-compiled, and creating many small functions is, by design, performant.
That tends to leave many small, simple functions, which need to be combined to carry out non-trivial tasks.
One obvious approach is to nest function calls. The following example is very contrived, but illustrates the point.
julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
The disadvantage of this approach is that readability drops rapidly as nesting gets deeper.
We need a simpler and more flexible approach.
This is the technique beloved of mathematicians, and Julia copies the mathematical syntax.
An arbitrary number of functions can be composed
together with β
operators (entered as \circ
then tab).
The result can be used as a single function.
julia> compfunc = first β titlecase β reverse
first β titlecase β reverse
julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
# alternative syntax, giving the same result
julia> (first β titlecase β reverse).(["my", "test", "strings"])
A couple of points to note:
An alternative might be thought of as the programmers' approach, rather than the mathematicians'.
Pipelines
have long been used in Unix shell scripts, and more recently became popular in mainstream programming languages (F# is sometimes credited with pioneering their adoption).
The basic concept is to start with some data, then pipe it through a sequence of functions to get the result.
The pipe operator is |>
(as in F# and recent versions of R), though Julia also has a broadcast version .|>
.
julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
Execution is now strictly left-to-right, with the output of each function flowing in the direction of the arrow to become the input for the next function.
It is no coincidence that the functions used to illustrate composition and pipelining all take a single argument.
Some purely-functional languages, pipe the first argument into a function but allow others to be included.
In contrast, Julia only expects function names (or something equivalent) in a pipeline, without any additional arguments.
There are important technical reasons for this (related to the fact that currying
is not a standard part of the language design).
The many people who have no understanding of currying should merely accept that this limitation is not a careless oversight, and is not likely to change in future Julia versions.
We need single-arguments functions that do whatever is needed. Fortunately, defining new functions in Julia is easy.
Most simply, we could use an anonymous function
.
For example, if we have a single input string and we want to split on underscores:
julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
That vector could then be piped to other functions, as before.
Enclosing the anonymous function in parentheses is optional in this case, but more generally is a useful way to reduce ambiguity.
Equally, we could create a named function, earlier in the program, and reuse it as needed.
In this exercise, you are going to help high school sweethearts profess their love on social media by generating a unicode heart with their initials:
β€ J. + M. β€
Implement the cleanupname
method.
It should take a name and remove all -
characters from it and replace them with a space.
It should also remove any whitespace from the beginning and end of the name.
julia> cleanupname("Jane-Ann")
"Jane Ann"
Implement the firstletter
method.
It should take a name and return its first letter as a string.
Make sure to reuse cleanupname
that you defined in the previous step and compose it with other functions.
julia> firstletter("Jane")
"J"
Implement the initial
method.
It should take a name and return its first letter, uppercase, followed by a dot.
Make sure to reuse firstletter
that you defined in the previous step.
initial("Robert")
"R."
Implement the couple
method.
It should take two names and return the initials with emoji hearts around.
Make sure to reuse initial
that you defined in the previous step.
couple("Blake Miller", "Riley Lewis")
"β€ B. + R. β€"
Sign up to Exercism to learn and master Julia with 18 concepts, 101 exercises, and real human mentoring, all for free.