Julia Workshop
1. Julia IDEs
1.1. Atom
https://atom.io/packages/language-julia
![](/data/QmVeXCK6ovK6N52szSSsx8kPFCAHQjUajXsrb8FSrVHQ8Z?content-type=image%2Fpng&filename=juno.png)
1.2. Visual Studio Code
https://github.com/JuliaEditorSupport/julia-vscode
![](/data/QmUJw47Ws1mQPpCh62iVHyhKBaEmXm8669g6i9pmkgDpCV?content-type=image%2Fpng&filename=vscode.png)
1.3. Jupyter
https://github.com/JuliaLang/IJulia.jl
![](/data/Qme3o3udU1LcgvSpwEc59yyuG1UgYTBSzWCRvjNZPV7n9T?content-type=image%2Fpng&filename=ijulia.png)
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
![](/data/Qmf8yPWHAwCenCfWLHmGPwB5xUrQnnQieNVHMZzhsQPetV?content-type=image%2Fjpeg&filename=duck.jpg)
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)
![](/data/QmPN78gT5vrF3CMSmniNPobVedAPAE9JUDJMRzYN2TDrzG?content-type=image%2Fgif&filename=puzzlenofit.gif)
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 😛
![](/data/QmdFn9PmLJafaDoK7aEPNHmdxSZ3YB2n5Y9Pyewa4wNqma?content-type=image%2Fpng&filename=puzzle.png)
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]