Home

Awesome

Functional Programming Jargon in Julia

Functional programming (FP) provides many advantages, and its popularity has been increasing as a result. However, each programming paradigm comes with its own unique jargon and FP is no exception. By providing a glossary, we hope to make learning FP easier.

This is a fork of Functional Programming Jargon. Examples are presented in Julia.

This document is WIP and pull requests are welcome!

Translations

Table of Contents

<!-- RM(noparent,notop) --> <!-- /RM -->

Arity

The number of arguments a function takes. From words like unary, binary, ternary, etc. This word has the distinction of being composed of two suffixes, "-ary" and "-ity." Addition, for example, takes two arguments, and so it is defined as a binary function or a function with an arity of two. Such a function may sometimes be called "dyadic" by people who prefer Greek roots to Latin. Likewise, a function that takes a variable number of arguments is called "variadic," whereas a binary function must be given two and only two arguments, currying and partial application notwithstanding (see below).

julia> myadd(x,y) = x + y

julia> arity = first(methods(myadd)).nargs - 1
2

# The arity of `myadd` is 2

Higher-Order Functions (HOF)

A function which takes a function as an argument and/or returns a function.

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

julia> sum(abs, [-1, -2, 3])
6

Closure

A closure is a way of accessing a variable outside its scope. Formally, a closure is a technique for implementing lexically scoped named binding. It is a way of storing a function with an environment.

A closure is a scope which captures local variables of a function for access even after the execution has moved out of the block in which it is defined. ie. they allow referencing a scope after the block in which the variables were declared has finished executing.

# this function returns an anonymous function that **closes over** `init`
julia> add_to(init) = x -> x + init

julia> add_to_five = add_to(5)

julia> add_to_five(3)
8

Anonymous function Vs Closure: A anonymous is essentially a function that is defined inline rather than the standard method of declaring functions. Lambdas can frequently be passed around as objects.

A closure is a function that encloses its surrounding state by referencing fields external to its body. The enclosed state remains across invocations of the closure.

Further reading/Sources

Partial Application

Partially applying a function means creating a new function by pre-filling some of the arguments to the original function.

julia> add3(a, b, c) = a + b + c
add3 (generic function with 1 method)

julia> partial(f, args...) = (x...) -> f(args..., x...)

julia> five_plus = partial(add3, 2, 3)

julia> five_plus(4)
9

julia> one_plus_plus = partial(add3, 1)
#1 (generic function with 1 method)

julia> one_plus_plus(2,3)
6

#special case of 2-arguments original function
julia> six_plus = Base.Fix1(+, 6)
(::Base.Fix1{typeof(+), Int64}) (generic function with 1 method)

julia> six_plus(7)
13

Partial application helps create simpler functions from more complex ones by baking in data when you have it. Curried functions are automatically partially applied.

Currying

The process of converting a function that takes multiple arguments into a function that takes them one at a time.

Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.

julia> curried_add(a) = b -> a + b

julia> curried_add(40)(2)
42

julia> add2 = curried_add(2)

julia> add2(10)
12

Auto Currying

Transforming a function that takes multiple arguments into one that if given less than its correct number of arguments returns a function that takes the rest. When the function gets the correct number of arguments it is then evaluated.

The Curries.jl package has an currying decorator which works this way.

julia> using Currier

julia> @curried add3(x, y, z) = x + y + z

julia> add3(1,2,3)
6

julia> add3(1,2)(3)
6

julia> add3(1)(2)(3)
6

Further reading

Function Composition

The act of putting two functions together to form a third function where the output of one function is the input of the other.

# `∘` can be typed with \circ<tab>
julia> floor_and_string = string ∘ floor

julia> floor_and_string(121.212121)
"121.0"

Continuation

At any given point in a program, the part of the code that's yet to be executed is known as a continuation.

julia> print_as_string(num) = println("Given $num")

julia> function add_one_and_continue(num, cc)
           result = num + 1
           cc(result)
       end

julia> add_one_and_continue(2, print_as_string)
Given 3

Purity

A function is pure if the return value is only determined by its input values, and does not produce side effects. Be very careful when using @pure in Julia.

julia> greet(name) = "Hi, $name"

julia> greet("Brianne")
"Hi, Brianne"

As opposed to each of the following:

julia> name = "Brianne"
"Brianne"

julia> greet() = "Hi, $name"
greet (generic function with 2 methods)

julia> greet()
"Hi, Brianne"

The above example's output is based on data stored outside of the function...

julia> greeting = nothing

julia> function greet(name)
         global greeting
         greeting = "Hi, $name"
         return nothing
       end

julia> greet("Brianne")

julia> greeting
"Hi, Brianne"

... and this one modifies state outside of the function.

Side effects

A function or expression is said to have a side effect if apart from returning a value, it interacts with (reads from or writes to) external mutable state.

println("IO is a side effect!")

Idempotent

A function is idempotent if reapplying it to its result does not produce a different result.

identity(identity(10))
# this relys on sort algorithm being stable
sort(sort(sort([2, 1])))

Point-Free Style

Writing functions where the definition does not explicitly identify the arguments used. This style usually requires currying or other Higher-Order functions. A.K.A Tacit programming.

# Given
julia> mymap = fn -> vec -> map(fn, vec)
julia> myadd = a -> b -> a + b

# Then
# Not points-free - `numbers` is an explicit argument
julia> increment_all = numbers -> mymap(myadd(1))(numbers)

julia> increment_all([4,7,8])
3-element Vector{Int64}:
 5
 8
 9

# Points-free - The list is an implicit argument
julia> increment_all2 = mymap(myadd(1))

julia> increment_all([4,7,8])
3-element Vector{Int64}:
 5
 8
 9

increment_all identifies and uses the parameter numbers, so it is not points-free. increment_all2 is written just by combining functions and values, making no mention of its arguments. It is points-free.

Points-free function definitions look just like normal assignments without function ... or blah -> .

Predicate

A predicate is a function that returns true or false for a given value. A common use of a predicate is as the callback for array filter.

julia> filter(>(2), [1, 2, 3, 4])
2-element Vector{Int64}:
 3
 4

Contracts

A contract specifies the obligations and guarantees of the behavior from a function or expression at runtime. This acts as a set of rules that are expected from the input and output of a function or expression, and errors are generally reported whenever a contract is violated.

julia> add1(num::Integer) = num + 1

julia> add1(num) = throw(ArgumentError("Contract violated: expected `num::Integer`"))

julia> add1(2)
3

julia> add1("blah")
ERROR: ArgumentError: Contract violated: expected `num::Integer`

Category

A category in category theory is a collection of objects and morphisms between them. In programming, typically types act as the objects and functions as morphisms.

To be a valid category 3 rules must be met:

  1. There must be an identity morphism that maps an object to itself. Where a is an object in some category, there must be a function from a -> a.
  2. Morphisms must compose. Where a, b, and c are objects in some category, and f is a morphism from a -> b, and g is a morphism from b -> c; g(f(x)) must be equivalent to (g • f)(x).
  3. Composition must be associative f • (g • h) is the same as (f • g) • h

Since these rules govern composition at very abstract level, category theory is great at uncovering new ways of composing things.

Further reading

Value

Anything that can be assigned to a variable.

('John', 30)
(name = 'Person', age = 30)
5
a -> a^2
[1.0]
missing

Constant

A variable that cannot be reassigned once defined.

const five = 5

Constants are referentially transparent. That is, they can be replaced with the values that they represent without affecting the result.

Functor

An object that implements a map function which, while running over each value in the object to produce a new object, adheres to two rules:

Preserves identity

object.map(x => x) ≍ object

Composable

object.map(compose(f, g)) ≍ object.map(g).map(f)

(f, g are arbitrary functions)

In Julia map takes the first argument as the function:

julia> map(identity, [1,2,3])
3-element Vector{Int64}:
 1
 2
 3

julia> map(identity, (1,2,3))
(1, 2, 3)

and

julia> f(x) = x+1

julia> g(x) = x*2

julia> map(f, map(g, [1,2,3]))
3-element Vector{Int64}:
 3
 5
 7

julia> map(f∘g, [1,2,3])
3-element Vector{Int64}:
 3
 5
 7

Pointed Functor

An object with an of function that puts any number of values into it. The following property of(f(x)) == of(x).map(f) must also hold for any pointed functor. Better explaination

# here we think of `Vector` and `Base.map` together as a functor:

julia> of(t::AbstractVector{T}, vals...) where T = T[vals...]

julia> of([], 3)
1-element Vector{Any}:
 3

julia> of(Int[], 3)
1-element Vector{Int64}:
 3

julia> of(Float64[], 3)
1-element Vector{Float64}:
 3.0

Lift

Lifting is when you take a value and put it into an object like a functor. If you lift a function into an Applicative Functor then you can make it work on values that are also in that functor.

Some implementations have a function called lift, or liftA2 to make it easier to run functions on functors.

julia> liftA2(fn) = (a, b) -> fn.(a, b) # cheating, rely on broadcast to figure out how to apply `fn`
liftA2 (generic function with 1 method)

julia> mult(a, b) = a * b
mult (generic function with 1 method)

julia> lifted_mult = liftA2(mult) # this function now works on functors (arrays)
#5 (generic function with 1 method)

julia> lifted_mult([1, 2], [3]) # [3, 6]
2-element Vector{Int64}:
 3
 6

julia> lifted_mult([1, 2], 3)
2-element Vector{Int64}:
 3
 6

julia> lifted_mult([1, 2], [3,4])
2-element Vector{Int64}:
 3
 8

Referential Transparency

An expression that can be replaced with its value without changing the behavior of the program is said to be referentially transparent.

greet() = "Hello World!"

Any invocation of greet() can be replaced with Hello World! hence greet is referentially transparent. In Julia this is trivially true for any immutable object.

Equational Reasoning

When an application is composed of expressions and devoid of side effects, truths about the system can be derived from the parts.

Lambda

An anonymous function that can be treated like a value.

julia> f(a) = a + 1
f (generic function with 1 method)

julia> a -> a + 1
#3 (generic function with 1 method)

Lambdas are often passed as arguments to Higher-Order functions.

julia> filter(x -> x^2 < 10, 1:5)
3-element Vector{Int64}:
 1
 2
 3

You can assign a lambda to a variable.

julia> addone = a -> a + 1
#1 (generic function with 1 method)

Lambda Calculus

A branch of mathematics that uses functions to create a universal model of computation.

Lazy evaluation

Lazy evaluation is a call-by-need evaluation mechanism that delays the evaluation of an expression until its value is needed. In functional languages, this allows for structures like infinite lists, which would not normally be available in an imperative language where the sequencing of commands is significant.

julia> struct myrand end

julia> Base.iterate(::myrand, state=nothing) = rand()

julia> first(myrand())
0.1381937787185451

Monoid

An object with a function that "combines" that object with another of the same type.

One simple monoid is the addition of numbers:

julia> 1 + 1
2

In this case number is the object and + is the function.

An "identity" value must also exist that when combined with a value doesn't change it.

The additive identity value for addition is 0, can also be obtained by zero():

julia> 1 + zero(1)
1

It's also required that the grouping of operations will not affect the result (associativity):

julia> 1 + (2 + 3) == (1 + 2) + 3
true

List concatenation also forms a monoid:

julia> vcat([1, 2], [3, 4])
4-element Vector{Int64}:
 1
 2
 3
 4

The identity value is empty Int array Int[] (can be obtained by empty())

julia> vcat([1, 2], empty([1,2]))
2-element Vector{Int64}:
 1
 2

If identity and compose functions are provided, functions themselves form a monoid:

# `foo` is a 1-argument function
foo∘identity ≍ identity∘foo ≍ foo

Monad

A monad is an object with of and chain functions. chain is like map except it un-nests the resulting nested object.

julia> of(t::AbstractVector{T}, vals...) where T = T[vals...]

julia> chain(fn, v) = mapreduce(fn, vcat, v)

julia> chain(split, of([], "cat dot", "fish bird"))
4-element Vector{SubString{String}}:
 "cat"
 "dot"
 "fish"
 "bird"

# Contrast to map
julia> map(split, of([], "cat dot", "fish bird"))
2-element Vector{Vector{SubString{String}}}:
 ["cat", "dot"]
 ["fish", "bird"]

of is also known as return in other functional languages. chain is also known as flatmap and bind in other languages.

Comonad

An object that has extract and extend functions. Ref is a natural condidate:

julia> extract(x::Ref) = x[]

julia> extend(x::T, f) where T<:Ref = T(f(x[]))

julia> extract(Ref(1.0))
1.0

Extend runs a function on the comonad. The function should return the same type as the comonad.

julia> extend(Ref(1.0), x->3) # notice the type is preserved correctly
Base.RefValue{Float64}(3.0)

Applicative Functor

An applicative functor is an object with an ap function. ap applies a function in the object to a value in another object of the same type.

julia> ap(fn, val) = fn(val)
ap (generic function with 1 method)

julia> ap(fns::Vector, vals) = map(ap, fns, vals)
ap (generic function with 2 methods)

# Example usage
julia> ap([a->a+1], [1])
1-element Vector{Int64}:
 2

This is useful if you have two objects and you want to apply a binary function to their contents.

# Arrays that you want to combine
arg1 = [1, 3]
arg2 = [4, 5]

# combining function - must be curried for this to work
julia> add(x) = y -> x + y

julia> partially_applied_adds = map(add, arg1)

This gives you an array of functions that you can call ap on to get the result:

julia> ap(partially_applied_adds, arg2)
2-element Vector{Int64}:
 5
 8

Morphism

A transformation function.

Endomorphism

A function where the input type is the same as the output.

# String -> String
julia> uppercase("Aasd")
"AASD"

# Number -> Number
julia> decrement(num::T)::T = num-1

Isomorphism

A pair of transformations between 2 types of objects that is structural in nature and no data is lost.

For example, 2D coordinates could be stored as an array [2,3] or object {x: 2, y: 3}.

julia> coords_to_pair(coords) = (coords.x, coords.y)

julia> pair_to_coords(pair) = (x=pair[1], y=pair[2])

julia> pair_to_coords(coords_to_pair((x=1, y=2)))
(x = 1, y = 2)

Homomorphism

A homomorphism is just a structure preserving map. Weaker than isomorphism in that it doesn't require bijectivity. In fact, a functor is just a homomorphism between categories as it preserves the original category's structure under the mapping.

Julia's map satisfy this (one-to-one/injective) unless the function is not "pure":

map(uppercase, ["oreos"]) == [uppercase("oreos")]

Catamorphism

A reduce_right function that applies a function against an accumulator and each value of the array (from right-to-left) to reduce it to a single value.

julia> sum_by_reduce(vec) = foldr(+, vec)

julia> sum_by_reduce([1,2,3])
6

Anamorphism

An unfold function. An unfold is the opposite of fold (reduce). It generates a list from a single value.

julia> function unfold(f, seed)
         function go(f, seed, acc)
           res = f(seed)
           return isnothing(res) ? acc : go(f, res[2], push!(acc, res[1]))
           end
         return go(f, seed, [])
         end
unfold (generic function with 1 method)

julia> countDown(n) = unfold(n->ifelse(n<=0, nothing, (n, n-1)), 3)
countDown (generic function with 1 method)

julia> countDown(3)
3-element Vector{Any}:
 3
 2
 1

Hylomorphism

The combination of anamorphism and catamorphism.

Paramorphism

A function just like reduce_right. However, there's a difference:

In paramorphism, your reducer's arguments are the current value, the reduction of all previous values, and the list of values that formed that reduction.


julia> function para(reducer, accumulator, elements)
         isempty(elements) && return accumulator
         head, tail... = elements
         return reducer(head, tail, para(reducer, accumulator, tail))
       end

julia> suffixes(lst) = para((x, xs, sxs) -> [[xs]; sxs], [], lst)

julia> suffixes([1,2,3,4,5])
5-element Vector{Any}:
 [2, 3, 4, 5]
 [3, 4, 5]
 [4, 5]
 [5]
 Int64[]

The third parameter in the reducer (in the above example, [[xs]; sxs]) is kind of like having a history of what got you to your current acc value.

Apomorphism

it's the opposite of paramorphism, just as anamorphism is the opposite of catamorphism. Whereas with paramorphism, you combine with access to the accumulator and what has been accumulated, apomorphism lets you unfold with the potential to return early.

Setoid

An object that has an equals function which can be used to compare other objects of the same type.

In Julia, just use isequal(obj1, obj2)

Semigroup

An object that has a concat function that combines it with another object of the same type.

# column-major
julia> vcat([1], [2])
2-element Vector{Int64}:
 1
 2

Foldable

An object that has a reduce function that applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value.

julia> sum_by_reduce(vec) = reduce(+, vec)

julia> sum_by_reduce([1,2,3])
6

Lens

A lens is a structure (often an object or function) that pairs a getter and a non-mutating setter for some other data structure.

A popular implementation is Accessors.jl

julia> using Accessors

# this is immutable
julia> struct T
            a
            b
        end

julia> obj = T("AA", "BB");

julia> obj.a = "aa"
ERROR: setfield!: immutable struct of type T cannot be changed

julia> lens = @optic _.a
(@optic _.a)

julia> lens(obj)
"AA"

julia> set(obj, lens, 2)
T(2, "BB")

julia> obj # the object was not mutated, instead an updated copy was created
T("AA", "BB")

julia> modify(lowercase, obj, lens)
T("aa", "BB")

Type Signatures

Often functions in JavaScript will include comments that indicate the types of their arguments and return values.

There's quite a bit of variance across the community but they often follow the following patterns:

# add :: int -> int -> int
myadd1 = (x::Int -> y::Int -> x + y)::Int

# increment :: int -> int
increment(x::Int)::Int = x + 1

Due to JIT, it's better to NOT over-specificy types, because it only reduces the genrality of your function without any speed gain, also see Multiple Dispatch for more idiomatic way to write parametric methods in Julia.

Further reading

Algebraic data type

A composite type made from putting other types together. Two common classes of algebraic types are sum and product.

Sum type

A Sum type is the combination of two types together into another one. It is called sum because the number of possible values in the result type is the sum of the input types.

In Julia, it's almost the same as Type Unions, albeit some implementation details and lack of pattern matching, but because Julia is a dynamic language (i.e. behavior always correct when you take a value out of a union typed container), the line is blurred.

julia> a = Union{Float64, Int}[1,2,3]
3-element Vector{Union{Float64, Int64}}:
 1
 2
 3

julia> typeof.(a)
3-element Vector{DataType}:
 Int64
 Int64
 Int64

julia> a = Union{Float64, Int}[1,2,3.0]
3-element Vector{Union{Float64, Int64}}:
 1
 2
 3.0

julia> typeof.(a)
3-element Vector{DataType}:
 Int64
 Int64
 Float64

Sum types are sometimes called union types, discriminated unions, or tagged unions.

See an implementation SumTypes.jl.

Product type

A product type combines types together in a way you're probably more familiar with:

point(p) = (x=p[1], y=p[2])

It's called a product because the total possible values of the data structure is the product of the different values. Many languages have a tuple type which is the simplest formulation of a product type.

See also Set theory.

Option

Option is a sum type with two cases often called Some and None.

Option is useful for composing functions that might not return a value.

help?> Some
search: Some something @something @showtime VersionNumber test_colorscheme DimensionMismatch sortperm

  Some{T}


  A wrapper type used in `Union{Some{T}, Nothing}` to distinguish between the absence of a value (`nothing`)
  and the presence of a `nothing` value (i.e. `Some(nothing)`).

  Use `something` to access the value wrapped by a Some object.

Option is also known as Maybe. Some is sometimes called Just. None is sometimes called Nothing (as in Julia).

Function

A function f :: A => B is an expression - often called arrow or lambda expression - with exactly one (immutable) parameter of type A and exactly one return value of type B. That value depends entirely on the argument, making functions context-independant, or referentially transparent. What is implied here is that a function must not produce any hidden side effects - a function is always pure, by definition. These properties make functions pleasant to work with: they are entirely deterministic and therefore predictable. Functions enable working with code as data, abstracting over behaviour:

julia> times2(n::Number) = (n * 2)::Number
times2 (generic function with 1 method)

julia> map(times2, [1, 2, 3])
3-element Vector{Int64}:
 2
 4
 6