Julia Workshop
1. Julia IDEs
1.1. Atom
https://atom.io/packages/language-julia
1.2. Visual Studio Code
https://github.com/JuliaEditorSupport/julia-vscode
1.3. Jupyter
https://github.com/JuliaLang/IJulia.jl
1.4. Nextjournal
you're looking at it right now!
- reproducible science in article form
- articles are interactive, fully versioned and automatically reproducible.
- supports Python, R, Clojure and Julia
# query the documentation of a function doc(+)
# Install Packages Pkg.pkg"add Plots" Base.disable_logging(Base.CoreLogging.Error) # disable warnings
# isolate code in Modules module PlotExample using Plots; pyplot() function example() plot(rand(100) / 3,reg = true,fill = (0, :green)) scatter!(rand(100), markersize = 6, c = :black) end end using .PlotExample PlotExample.example()
2. Julia in one Cell
# Function defined for all types T function func(a::T) where T T end abstract type SuperType end # inheritance struct MyType <: SuperType field::Int end # mutable type without inheritance but with Type Parameter mutable struct OtherType{TypeParam} field::TypeParam end # short function form func(a::Float64, b::SuperType) = "a string" func(b::OtherType{T}) where T = T func(b::OtherType{Int}) = "oh hello there" # tuple type func(f, a::Tuple{Any, Any}) = (f(a[1]), f(a[2])) func(1.0, MyType(1)) func(OtherType(1im)) func(OtherType(1)) # passing function tuple = (OtherType(0), OtherType(0)) func(func, tuple) # passing closure closure = x-> x * 2 func(closure, (2, 3)) # the same as above, but with do syntax result = func((2, 3)) do x x * 2 end result # construct array array = [OtherType(1.0), OtherType(1.0)] # dot syntax - apply a function to all elements of the argument arrays func.(array) nothing
3. Julia Core Strengths
1) Multiple Dispatch + Introspection
2) Efficient Duck Typing
3) Efficient Higher order Functions
4) Interprocedural Optimization
5) Dynamism + Meta programing
==> Performance + High Level programing
3.1. Multiple Dispatch + Introspection
There are three core concepts to multiple dispatch:
- dispatch on all argument types
- specialize method body to argument types
- types are compile time constants
dispatch(x::Int) = 1 dispatch(x::String) = 2 dispatch(x::Complex) = 3 test() = dispatch(1) + dispatch("2") + dispatch(1im)
using InteractiveUtils """ Helper to show the lowered code of a function""" function show_asts(f, args...) argtypes = typeof(args) println("$f($args):") println(code_typed(f, argtypes, optimize = false)[1][1]) println("optimized:") println(code_typed(f, argtypes)[1][1]) println("------------------------") return end
show_asts(dispatch, 1)
show_asts(test)
function dispatch(x, ::Nothing) isa(x, Int) && return 1 isa(x, String) && return 2 isa(x, Complex) && return 3 end test(x) = dispatch(1, x) + dispatch("2", x) + dispatch(1im, x)
show_asts(dispatch, 1, nothing)
show_asts(test, nothing)
methods(dispatch)
# generic base my_func(a, b) = a + b # in a package were you have more information: my_func(a::Float32, b::Float32) = Intrinsics.fmul(a, b)
3.1.1. Enables you to:
- compiled code with static type information inside function body
- offer generic fallbacks for all kind of type combinations
- specialize to type combinations
- add new methods to existing libraries
- efficient duck typing
3.2. Duck Typing
In duck typing, an object's suitability is determined by the presence of certain methods and properties, rather than the type of the object itself.
function do_something(a) quack(a) end struct Duck end quack(::Duck) = println("quoaak") # by overloading a function, we get quacking & hence can `do_something` quack(x::Int) = println("quack") do_something(Duck()) do_something(1)
3.2.1. Practical Examples
3.2.1.1. Algorithm Visualization
# untyped generic code: perfect to inject our own types :) function magic(y::AbstractArray, func) l = length(y) k = ceil(Int, log2(l)) for j=1:k, i=2^j:2^j:min(l, 2^k) y[i] = func(y[i-2^(j-1)], y[i]) end for j=(k-1):-1:1, i=3*2^(j-1):2^j:min(l, 2^k) y[i] = func(y[i-2^(j-1)], y[i]) end y end X = rand(10) Y = magic(copy(X), +)
import Base: getindex, setindex!, length, size mutable struct AccessArray <: AbstractArray{Nothing, 1} length::Int read::Vector history::Vector end function AccessArray(length, read = [], history = []) AccessArray(length, read, history) end length(A::AccessArray) = A.length size(A::AccessArray) = (A.length,) function getindex(A::AccessArray, i) push!(A.read, i) nothing end function setindex!(A::AccessArray, x, i) push!(A.history, (A.read, [i])) A.read = [] end import Base.+ +(a::Nothing, b::Nothing) = a
# we can now simply feed this new arrya type into magic - and record what it does A = magic(AccessArray(8), +)
using GeometryTypes, Makie function render(A::AccessArray) olast = depth = 0 for y in A.history (any(y[1] .≤ olast)) && (depth += 1) olast = maximum(y[2]) end maxdepth = depth olast = depth = 0 C = [] for y in A.history (any(y[1] .≤ olast)) && (depth += 1) push!(C, ((y...,), A.length, maxdepth, depth)) olast = maximum(y[2]) end msize = 0.1 outsize = 0.15 x1 = Point2f0.(first.(first.(first.(C))), last.(C) .+ outsize .+ 0.05) x2 = Point2f0.(last.(first.(first.(C))), last.(C) .+ outsize .+ 0.05) x3 = Point2f0.(first.(last.(first.(C))), last.(C) .+ 1) connections = Point2f0[] yoff = Point2f0(0, msize / 2) ooff = Point2f0(0, outsize / 2 + 0.05) for i = 1:length(x3) push!(connections, x3[i] .- ooff, x1[i] .+ yoff, x3[i] .- ooff, x2[i] .+ yoff) end node_theme = NT( markersize = msize, strokewidth = 3, strokecolor = :black, color = (:white, 0.0), axis = NT( ticks = NT(ranges = (1:8, 1:5)), names = NT(axisnames = ("Array Index", "Depth")), frame = NT(axis_position = :none) ) ) s = Scene(resolution = (500, 500)) s = scatter!(s, node_theme, x1) s = scatter!(s, node_theme, x2) scatter!(x3, color = :white, markersize = 0.2, strokewidth = 4, strokecolor = :black) scatter!(x3, color = :red, marker = '+', markersize = outsize) linesegments!(connections, color = :red) s end render(A)
# Check our theory: Y - [0; Y[1:end-1];] ≈ X
[example adapted from Jiahao Che n]
3.2.1.2. Automatic Differentiation
using ForwardDiff f(x) = x^2 + 2.0x + 3.0 f_grad(x) = 2x + 2 # manual gradient function val = 5 dual = ForwardDiff.Dual(val, 1) dx = f(dual) ForwardDiff.value(dx) == f(val) ForwardDiff.partials(dx)[] == f_grad(val)
using BenchmarkTools vals = fill(val, 10^6) duals = ForwardDiff.Dual.(vals, 1) f($val) f($dual) f.($vals) f.($duals) nothing
3.2.2. Enables you to
- compose functionality across packages
- low effort to extend packages
- use your own types that do extra work with generic APIs
3.3. Efficient higher order function
using BenchmarkTools # we annotate mutating functions usually with '!' function map1!(f::F, dest, A) where F for i in 1:length(dest) dest[i] = f(A[i]) end return dest end # This is usually how other languages implement higher order functions function map2!( (f), dest, A) for i in 1:length(dest) dest[i] = f(A[i]) end return dest end # some heavy functions test1(x) = x / 10 test2(x) = test1(x)# some heavy functions A, dest = rand(10^6), zeros(10^6); map1!($test1, $dest, $A) map1!($test2, $dest, $A) map2!($test1, $dest, $A) nothing
3.3.1. Enables you to:
- implement mapreduce workflows with optimal performance
- compose functionality freely
3.4. Interprocedural optimization (IPO)
3.4.1. What would Python do?
import Solver # Probably uses Numpy(C) + Numba import Arithmetic # Maybe CPython + Python? class MyType: ... # CPython vs Numba, # Numpy & CPython vs MyClass # some_function written in pure python? -> no specialization Solver.solve(Arithmetic.some_function, convert(MyType, data))
Making a single Python algorithm fast in isolation is not that hard
making those algorithms generic and interface with other packages is hard!
- use cython - loose generics and most higher level constructs & needs compilation
- use numba - loose substantial subset of language, no IPO
- use C & Python - loose everything 😛
using Solver, Arithmetic, GPUArrays, PrecisionFloats data = convert(PrecisionFloats.Float, some_float_data) # fast higher order functions, duck typing, # constant propagation + inlining across packages Solver.solve(Arithmetic.some_function, GPUArray(data)) # Solver doesn't even need to know Arithmetic nor GPUArrays,
3.5. Dynamics
str = "1 + 1" expr = Meta.parse(str) eval(expr)
expr = quote 1 + 1 end # or the short form expr = :(1 + 1) dump(expr) # dump = detailed output of fields
eval(Expr(:call, +, 1, 1))
macro replace_ab(expr, val) map!(expr.args, expr.args) do arg arg in (:a, :b) && return val arg end return expr end (a * b, "hi ")
3.5.1. Performance of dynamic constructs
3.5.1.1. Macros - no cost
Macros are just a code transformation on the syntax level
3.5.1.2. Eval - scoped performance penalty
using BenchmarkTools function worst(a, expr::Expr) f = eval(expr) map!(a, a) do val Base.invokelatest(f, val) end end function best(f, a) map!(f, a, a) end x = rand(10^6) expr = :((a)-> cos(a)) worst($x, expr) efunc = eval(:((a)-> cos(a))) best($efunc, $x) nothing
test(x) = eval(x) show_asts(test, :(1 + 1))
3.5.2. Generated Functions
function test(a, b) # Println is not really allowed here - Core.println works though Core.println(a, " ", b) return :() # Need to return an Expr end
test(1, 2)
function test(a, b) Core.println(a, " ", b) return :(a + b) # Need to return an Expr end test(1, 2) test(1, 2)
expressions = [] function map_unrolled(f, arg::NTuple{N, Any}) where N args = map(1:N) do i :(f(arg[$i])) end expr = Expr(:tuple, args...) push!(expressions, expr) # Core.println is really ugly for Expr expr end
map_unrolled(sin, (1.0, 2.0, 3.0))
expressions[1]