using Pkg
Pkg.activate(".")
Pkg.instantiate()
Activating environment at `~/Dropbox/Presentations/2020-detroit-tech-watch-julia-intro/Project.toml`
Follow along at: https://tinyurl.com/tech-watch-julia [1]
[1] https://github.com/rdeits/DetroitTechWatch2020.jl/blob/master/Intro%20to%20Julia.ipynb
https://docs.julialang.org/en/v1/
Arithmetic:
2 + 2
4
Strings:
# Strings
println("hello world")
hello world
Arrays:
x = [1, 2, 3, 4];
function say_hello(name)
println("hello ", name)
end
say_hello (generic function with 1 method)
say_hello("world")
hello world
By default, a function is generic, so you can pass in any type you want:
say_hello([1, 2, 3])
hello [1, 2, 3]
Every value in Julia has a type:
typeof(1.0)
Float64
typeof(1)
Int64
typeof(π)
Irrational{:π}
typeof([1, 2, 3])
Array{Int64,1}
You can create your own types to organize your data:
struct Person
name::String
end
alice = Person("Alice")
Person("Alice")
Julia's types are extremely lightweight, and user-defined types are exactly as performant as anything built-in:
sizeof(Person) == sizeof(Ptr{String})
true
Julia does not have classes like Java, Python, or C++. Instead, code is organized around multiple dispatch, where the compiler chooses the appropriate method of a given function based on the types of all of its input arguments.
For more, see: The Unreasonable Effectiveness of Multiple Dispatch (Stefan Karpinski, JuliaCon 2019)
greet(x, y) = println("$x greets $y")
greet (generic function with 1 method)
alice = Person("alice")
bob = Person("bob")
greet(alice, bob)
Person("alice") greets Person("bob")
Currently there is only one greet()
function, and it will work on x
and y
of any type:
greet([1, 2, 3], "hello world")
[1, 2, 3] greets hello world
We can use abstract types to organize the behavior of related types:
abstract type Animal end
struct Cat <: Animal
name::String
end
We've already defined greet(x, y)
for any x
and y
, but we can add another definition for a more specific set of input types.
We can be as specific or as general as we like with the argument types:
greet(x::Person, y::Animal) = println("$x pats $y")
greet (generic function with 2 methods)
greet(x::Cat, y) = println("$x meows at $y")
greet (generic function with 3 methods)
Julia will always pick the most specific method that matches the provided function arguments.
fluffy = Cat("fluffy")
greet(alice, fluffy)
Person("alice") pats Cat("fluffy")
greet(fluffy, alice)
Cat("fluffy") meows at Person("alice")
struct Dog <: Animal
name::String
end
greet(x::Dog, y) = println("$x barks at $y")
greet(x::Dog, y::Person) = println("$x licks $y's face")
greet(x::Dog, y::Dog) = println("$x sniffs $y's butt")
greet (generic function with 6 methods)
fido = Dog("fido")
rex = Dog("rex")
greet(alice, fido)
Person("alice") pats Dog("fido")
greet(fido, fluffy)
Dog("fido") barks at Cat("fluffy")
greet(fido, bob)
Dog("fido") licks Person("bob")'s face
greet(fido, rex)
Dog("fido") sniffs Dog("rex")'s butt
If you want to know which greet
method will be called for a given set of arguments, you can use @which
to check:
@which greet(alice, fido)
You can list all of the methods of a given function with methods
:
methods(greet)
Modules in Julia are used to organize code into namespaces.
module MyUsefulModule
export hello
hello() = println("hello world")
goodbye() = println("goodbye world")
end
MyUsefulModule.hello()
hello world
The using
command brings any export
ed symbols from a module into the current namespace:
using .MyUsefulModule
hello()
hello world
Julia has a built-in package manager called Pkg
. It handles installing packages and managing all your package environments.
A package environment represents a single set of installed packages. Let's activate the environment for this talk:
using Pkg
Pkg.activate(".")
Activating environment at `~/Dropbox/Presentations/2020-detroit-tech-watch-julia-intro/Project.toml`
(this is similar to source venv/bin/activate
in a Python virtual environment)
We can install a package in our current environment. This will only affect that environment, so we can safely do this without breaking any other Julia projects we might be working on:
Pkg.add("Colors")
Updating registry at `~/.julia/registries/General`
Updating git-repo `git@github.com:JuliaRegistries/General.git`
Resolving package versions... Updating `~/Dropbox/Presentations/2020-detroit-tech-watch-julia-intro/Project.toml` [no changes] Updating `~/Dropbox/Presentations/2020-detroit-tech-watch-julia-intro/Manifest.toml` [no changes]
The Project.toml
file gives a concise description of the packages we've added to this environment:
run(`cat Project.toml`)
[deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
Process(`cat Project.toml`, ProcessExited(0))
The package manager also generates a complete manifest of every package that is installed, including all the transitive dependencies and their versions. You can use this to reproduce a given package environment exactly:
run(`cat Manifest.toml`)
# This file is machine-generated - editing it directly is not advised [[Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" [[BenchmarkTools]] deps = ["JSON", "Logging", "Printf", "Statistics", "UUIDs"] git-tree-sha1 = "9e62e66db34540a0c919d72172cc2f642ac71260" uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" version = "0.5.0" [[Bzip2_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "3663bfffede2ef41358b6fc2e1d8a6d50b3c3904" uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" version = "1.0.6+2" [[ColorSchemes]] deps = ["ColorTypes", "Colors", "FixedPointNumbers", "Random", "StaticArrays"] git-tree-sha1 = "7a15e3690529fd1042f0ab954dff7445b1efc8a5" uuid = "35d6a980-a343-548e-a6ea-1d62b119f2f4" version = "3.9.0" [[ColorTypes]] deps = ["FixedPointNumbers", "Random"] git-tree-sha1 = "c73d9cfc2a9d8433dc77f5bff4bddf46b1d78c20" uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" version = "0.10.3" [[Colors]] deps = ["ColorTypes", "FixedPointNumbers", "InteractiveUtils", "Reexport"] git-tree-sha1 = "2fdeb981ebcf52cd800ddb6a0aa5eac34153552d" uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" version = "0.12.0" [[Conda]] deps = ["JSON", "VersionParsing"] git-tree-sha1 = "7a58bb32ce5d85f8bf7559aa7c2842f9aecf52fc" uuid = "8f4d0f93-b110-5947-807f-2305c1781a2d" version = "1.4.1" [[Contour]] deps = ["StaticArrays"] git-tree-sha1 = "0b17db36e7e03f8437e0d1f55aea3e4a60c74353" uuid = "d38c429a-6771-53c6-b99e-75d170b6e991" version = "0.5.3" [[DataAPI]] git-tree-sha1 = "176e23402d80e7743fc26c19c681bfb11246af32" uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" version = "1.3.0" [[DataStructures]] deps = ["InteractiveUtils", "OrderedCollections"] git-tree-sha1 = "af6d9c86e191c917c2276fbede1137e8ea20157f" uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" version = "0.17.17" [[Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" [[DelimitedFiles]] deps = ["Mmap"] uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" [[Distributed]] deps = ["Random", "Serialization", "Sockets"] uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" [[FFMPEG]] deps = ["FFMPEG_jll"] git-tree-sha1 = "c82bef6fc01e30d500f588cd01d29bdd44f1924e" uuid = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" version = "0.3.0" [[FFMPEG_jll]] deps = ["Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "LAME_jll", "LibVPX_jll", "Libdl", "Ogg_jll", "OpenSSL_jll", "Opus_jll", "Pkg", "Zlib_jll", "libass_jll", "libfdk_aac_jll", "libvorbis_jll", "x264_jll", "x265_jll"] git-tree-sha1 = "0fa07f43e5609ea54848b82b4bb330b250e9645b" uuid = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" version = "4.1.0+3" [[FixedPointNumbers]] git-tree-sha1 = "3ba9ea634d4c8b289d590403b4a06f8e227a6238" uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" version = "0.8.0" [[FreeType2_jll]] deps = ["Bzip2_jll", "Libdl", "Pkg", "Zlib_jll"] git-tree-sha1 = "7d900f32a3788d4eacac2bfa3bf5c770179c8afd" uuid = "d7e528f0-a631-5988-bf34-fe36492bcfd7" version = "2.10.1+2" [[FriBidi_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "2f56bee16bd0151de7b6a1eeea2ced190a2ad8d4" uuid = "559328eb-81f9-559d-9380-de523a88c83c" version = "1.0.5+3" [[GR]] deps = ["Base64", "DelimitedFiles", "LinearAlgebra", "Printf", "Random", "Serialization", "Sockets", "Test", "UUIDs"] git-tree-sha1 = "1185d50c5c90ec7c0784af7f8d0d1a600750dc4d" uuid = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" version = "0.49.1" [[GeometryTypes]] deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "StaticArrays"] git-tree-sha1 = "34bfa994967e893ab2f17b864eec221b3521ba4d" uuid = "4d00f742-c7ba-57c2-abde-4428a4b178cb" version = "0.8.3" [[HTTP]] deps = ["Base64", "Dates", "IniFile", "MbedTLS", "Sockets"] git-tree-sha1 = "fe31f4ff144392ad8176f5c7c03cca6ba320271c" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" version = "0.8.14" [[IniFile]] deps = ["Test"] git-tree-sha1 = "098e4d2c533924c921f9f9847274f2ad89e018b8" uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" version = "0.5.0" [[InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" [[JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" version = "0.21.0" [[LAME_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "221cc8998b9060677448cbb6375f00032554c4fd" uuid = "c1c5ebd0-6772-5130-a774-d5fcae4a789d" version = "3.100.0+1" [[LibGit2]] deps = ["Printf"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" [[LibVPX_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "e3549ca9bf35feb9d9d954f4c6a9032e92f46e7c" uuid = "dd192d2f-8180-539f-9fb4-cc70b1dcf69a" version = "1.8.1+1" [[Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" [[LinearAlgebra]] deps = ["Libdl"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" [[Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" [[MacroTools]] deps = ["Markdown", "Random"] git-tree-sha1 = "f7d2e3f654af75f01ec49be82c231c382214223a" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" version = "0.5.5" [[Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" [[MbedTLS]] deps = ["Dates", "MbedTLS_jll", "Random", "Sockets"] git-tree-sha1 = "426a6978b03a97ceb7ead77775a1da066343ec6e" uuid = "739be429-bea8-5141-9913-cc70e7f3736d" version = "1.0.2" [[MbedTLS_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "c83f5a1d038f034ad0549f9ee4d5fac3fb429e33" uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" version = "2.16.0+2" [[Measures]] git-tree-sha1 = "e498ddeee6f9fdb4551ce855a46f54dbd900245f" uuid = "442fdcdd-2543-5da2-b0f3-8c86c306513e" version = "0.3.1" [[Missings]] deps = ["DataAPI"] git-tree-sha1 = "de0a5ce9e5289f27df672ffabef4d1e5861247d5" uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" version = "0.4.3" [[Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" [[NaNMath]] git-tree-sha1 = "928b8ca9b2791081dc71a51c55347c27c618760f" uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" version = "0.3.3" [[Ogg_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "59cf7a95bf5ac39feac80b796e0f39f9d69dc887" uuid = "e7412a2a-1a6e-54c0-be00-318e2571c051" version = "1.3.4+0" [[OpenSSL_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "d2a6f25262d568b5a7e454cf7ff5066a79d16c7d" uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" version = "1.1.1+2" [[Opus_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "002c18f222a542907e16c83c64a1338992da7e2c" uuid = "91d4177d-7536-5919-b921-800302f37372" version = "1.3.1+1" [[OrderedCollections]] git-tree-sha1 = "12ce190210d278e12644bcadf5b21cbdcf225cd3" uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" version = "1.2.0" [[Parsers]] deps = ["Dates", "Test"] git-tree-sha1 = "f0abb338b4d00306500056a3fd44c221b8473ef2" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" version = "1.0.4" [[Pkg]] deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [[PlotThemes]] deps = ["PlotUtils", "Requires", "Statistics"] git-tree-sha1 = "c6f5ea535551b3b16835134697f0c65d06c94b91" uuid = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" version = "2.0.0" [[PlotUtils]] deps = ["ColorSchemes", "Colors", "Dates", "Printf", "Random", "Reexport", "Statistics"] git-tree-sha1 = "59ec24a0c96c513533e488dff1433df1bd3d6b9f" uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043" version = "1.0.3" [[Plots]] deps = ["Base64", "Contour", "Dates", "FFMPEG", "FixedPointNumbers", "GR", "GeometryTypes", "JSON", "LinearAlgebra", "Measures", "NaNMath", "Pkg", "PlotThemes", "PlotUtils", "Printf", "REPL", "Random", "RecipesBase", "RecipesPipeline", "Reexport", "Requires", "Showoff", "SparseArrays", "Statistics", "StatsBase", "UUIDs"] git-tree-sha1 = "64e7405da4333ee6df59d7d0d88aade456341b3e" uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" version = "1.3.2" [[Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" [[ProgressMeter]] deps = ["Distributed", "Printf"] git-tree-sha1 = "b3cb8834eee5410c7246734cc6f4f586fe0dc50e" uuid = "92933f4c-e287-5a05-a399-4b506db050ca" version = "1.3.0" [[PyCall]] deps = ["Conda", "Dates", "Libdl", "LinearAlgebra", "MacroTools", "Serialization", "VersionParsing"] git-tree-sha1 = "3a3fdb9000d35958c9ba2323ca7c4958901f115d" uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" version = "1.91.4" [[REPL]] deps = ["InteractiveUtils", "Markdown", "Sockets"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [[Random]] deps = ["Serialization"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [[RecipesBase]] git-tree-sha1 = "54f8ceb165a0f6d083f0d12cb4996f5367c6edbc" uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" version = "1.0.1" [[RecipesPipeline]] deps = ["Dates", "PlotUtils", "RecipesBase"] git-tree-sha1 = "9215637e28503ca85bef843a1fc02b2f76f1ba09" uuid = "01d81517-befc-4cb6-b9ec-a95719d0359c" version = "0.1.9" [[Reexport]] deps = ["Pkg"] git-tree-sha1 = "7b1d07f411bc8ddb7977ec7f377b97b158514fe0" uuid = "189a3867-3050-52da-a836-e630ba90ab69" version = "0.2.0" [[Requires]] deps = ["UUIDs"] git-tree-sha1 = "d37400976e98018ee840e0ca4f9d20baa231dc6b" uuid = "ae029012-a4dd-5104-9daa-d747884805df" version = "1.0.1" [[SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" [[Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [[Showoff]] deps = ["Dates"] git-tree-sha1 = "e032c9df551fb23c9f98ae1064de074111b7bc39" uuid = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" version = "0.3.1" [[Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" [[SortingAlgorithms]] deps = ["DataStructures", "Random", "Test"] git-tree-sha1 = "03f5898c9959f8115e30bc7226ada7d0df554ddd" uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" version = "0.3.1" [[SparseArrays]] deps = ["LinearAlgebra", "Random"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [[StaticArrays]] deps = ["LinearAlgebra", "Random", "Statistics"] git-tree-sha1 = "5c06c0aeb81bef54aed4b3f446847905eb6cbda0" uuid = "90137ffa-7385-5640-81b9-e52037218182" version = "0.12.3" [[Statistics]] deps = ["LinearAlgebra", "SparseArrays"] uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [[StatsBase]] deps = ["DataAPI", "DataStructures", "LinearAlgebra", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics"] git-tree-sha1 = "a6102b1f364befdb05746f386b67c6b7e3262c45" uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" version = "0.33.0" [[Test]] deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [[UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [[Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" [[VersionParsing]] git-tree-sha1 = "80229be1f670524750d905f8fc8148e5a8c4537f" uuid = "81def892-9a0e-5fdd-b105-ffc91e053289" version = "1.2.0" [[Zlib_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "a2e0d558f6031002e380a90613b199e37a8565bf" uuid = "83775a58-1f1d-513f-b197-d71354ab007a" version = "1.2.11+10" [[libass_jll]] deps = ["Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "Libdl", "Pkg", "Zlib_jll"] git-tree-sha1 = "027a304b2a90de84f690949a21f94e5ae0f92c73" uuid = "0ac62f75-1d6f-5e53-bd7c-93b484bb37c0" version = "0.14.0+2" [[libfdk_aac_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "480c7ed04f68ea3edd4c757f5db5b6a0a4e0bd99" uuid = "f638f0a6-7fb0-5443-88ba-1cc74229b280" version = "0.1.6+2" [[libvorbis_jll]] deps = ["Libdl", "Ogg_jll", "Pkg"] git-tree-sha1 = "6a66f65b5275dfa799036c8a3a26616a0a271c4a" uuid = "f27f6e37-5d2b-51aa-960f-b287f2bc3b7a" version = "1.3.6+4" [[x264_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "d89346fe63a6465a9f44e958ac0e3d366af90b74" uuid = "1270edf5-f2f9-52d2-97e9-ab00b5d0237a" version = "2019.5.25+2" [[x265_jll]] deps = ["Libdl", "Pkg"] git-tree-sha1 = "61324ad346b00a6e541896b94201c9426591e43a" uuid = "dfaa095f-4041-5dcd-9319-2fabd8486b76" version = "3.0.0+1"
Process(`cat Manifest.toml`, ProcessExited(0))
Let's take a tour of one of my favorite packages, Colors.jl, and show off some feature's of Julia's arrays along the way.
We can load a package from the current environment with using
:
using Colors: RGB # For now, just bring the `RGB` name into scope
RGB(1, 0, 0)
The RGB
type from Colors.jl knows how to render itself as an actual colored div
when running in Jupyter. We can also print its value as a string if we want:
print(RGB(1, 0, 0))
RGB{N0f8}(1.0,0.0,0.0)
Julia arrays are fully generic, so we can create an array of colors:
C = [RGB(i, j, 0) for i in 0:0.1:1, j in 0:0.1:1]
typeof(C)
Array{RGB{Float64},2}
C is an array like any other, so we can index into it and slice it:
C[8, 2]
C[1, :]
Let's pull out the red channel from our image C
:
using Colors: red, green, blue
If you don't know what a function does, you can use the ?
operator to access its docstring:
?red
search: reduce redisplay redirect_stdin redirect_stdout redirect_stderr
red(c)
returns the red component of an AbstractRGB
opaque or transparent color.
red(C[1, 1])
0.0
To get the red channel of each element of C
, we can use broadcasting. The syntax f.(x)
applies the function f
to each element of x
:
red.(C)
11×11 Array{Float64,2}: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.3 0.3 0.3 0.3 0.3 0.3 0.3 0.3 0.3 0.3 0.3 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.7 0.7 0.7 0.7 0.7 0.7 0.7 0.7 0.7 0.7 0.7 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.9 0.9 0.9 0.9 0.9 0.9 0.9 0.9 0.9 0.9 0.9 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
That's not very visual. Let's render that red channel as a grayscale image:
using Colors: Gray
Gray.(red.(C))
Julia's broadcasting provides guaranteed loop fusion. That means that if you do Gray.(red.(x))
, the language guarantees that it will do only one loop over the elements of x
, computing Gray(red(x_i))
for each x_i
in x
.
See https://julialang.org/blog/2017/01/moredots/ for more.
sum
function in Julia, C, and Python so we can compare them:Let's start with Julia:
"""
Naive implementation of sum. Works for any iterable `x` with any element type.
"""
function my_sum(x)
result = zero(eltype(x))
for element in x
result += element
end
return result
end
my_sum
And let's create some data to test with:
data = rand(Float64, 10^7)
10000000-element Array{Float64,1}: 0.3250821881143635 0.5739150786758722 0.13383939143899348 0.4395993297446783 0.3295526596520042 0.02847425323560948 0.8487199564066177 0.7997505161145844 0.6245936015647562 0.5106550585869152 0.9238301866509853 0.49764889129935574 0.7401243526305121 ⋮ 0.6680583163473712 0.4697749293985751 0.8080600012381363 0.08649714310431955 0.2412430060343358 0.33645492148197076 0.138424161003317 0.27552196839070064 0.00438248390256013 0.7495825118700372 0.11786299612959139 0.9934003189851528
To measure the performance of my_sum
, we'll use the BenchmarkTools.jl package.
using BenchmarkTools
@benchmark my_sum($data)
BenchmarkTools.Trial: memory estimate: 0 bytes allocs estimate: 0 -------------- minimum time: 11.994 ms (0.00% GC) median time: 12.054 ms (0.00% GC) mean time: 12.099 ms (0.00% GC) maximum time: 16.589 ms (0.00% GC) -------------- samples: 414 evals/sample: 1
In this case, we only care about the minimum time. The @btime
macro is a shorthand to print just that minimum time:
@btime my_sum($data)
11.716 ms (0 allocations: 0 bytes)
4.998996907719521e6
Let's compare this with C. It's easy to call functions from C shared libraries in Julia:
"""
Call the `strcmp` function from `libc.so.6`
"""
function c_compare(x::String, y::String)
# We have to tell the compiler that this C function returns an `int` and
# expects two `char *` inputs. The `Cint` and `Cstring` types are convenient
# shorthands for those:
ccall((:strcmp, "libc.so.6"), Cint, (Cstring, Cstring), x, y)
end
c_compare
c_compare("hello", "hello")
0
Calling C functions has very little overhead:
@btime c_compare($("hello"), $("hello"))
8.760 ns (0 allocations: 0 bytes)
0
Let's create a C implementation of my_sum
. We can do that without leaving Julia by piping some code directly to GCC:
C_code = """
#include <stddef.h> // For `size_t`
// Note: our Julia code works for any type, but the C implementation
// is only for `double`.
double c_sum(size_t n, double *X) {
double s = 0.0;
size_t i;
for (i = 0; i < n; ++i) {
s += X[i];
}
return s;
}
""";
Now let's generate a name for our shared library:
# dlext gives the correct file extension for a shared library on this platform
using Libdl: dlext
const Clib = tempname() * "." * dlext
"/tmp/jl_FELUh5.so"
To send the code to GCC, we can use open()
on a command to write directly to the stdin
of that command as if it were any other file- or buffer-like object:
open(`gcc -fPIC -O3 -msse3 -xc -shared -o $Clib -`, "w") do cmd
print(cmd, C_code)
end
Now we can define a Julia function that calls the C function we just compiled:
# The return type and argument types must match the signature we declared above:
#
# double c_sum(size_t n, double *X)
#
c_sum(X::Array{Float64}) = ccall(("c_sum", Clib), Cdouble, (Csize_t, Ptr{Cdouble}), length(X), X)
c_sum (generic function with 1 method)
Now let's measure the performance of the pure C function:
@btime c_sum($data)
11.360 ms (0 allocations: 0 bytes)
4.998996907719521e6
Let's plot the result using the Plots.jl package:
using Plots
results = [
"my_sum (Julia)" => 11.7,
"c_sum (C)" => 11.3
]
bar(first.(results), last.(results), xlabel="function", ylabel="time (ms, shorter is better)", legend=nothing)
Our naive Julia code is just as fast as our naive C code!
Is that as fast as we can go? What about Julia's built-in sum()
function:
@btime sum($data)
5.377 ms (0 allocations: 0 bytes)
4.998996907718867e6
results = [
"my_sum (Julia)" => 11.7,
"c_sum (C)" => 11.3,
"sum (Julia)" => 5.4,
]
bar(first.(results), last.(results), xlabel="function", ylabel="time (ms, shorter is better)", legend=nothing)
What's going on? Is the sum()
function using some built-in behavior we don't have access to?
Nope--we can achieve that result easily with a few modifications:
function my_fast_sum(x)
result = zero(eltype(x))
# `@inbounds` is a macro which disables all bounds checking within a given block.
#
# `@simd` enables additional vector operations by indicating that it is OK to potentially
# evaluate the loop out-of-order.
@inbounds @simd for element in x
result += element
end
result
end
my_fast_sum (generic function with 1 method)
@btime my_fast_sum($data)
5.332 ms (0 allocations: 0 bytes)
4.998996907718901e6
results = [
"my_sum (Julia)" => 11.7,
"c_sum (C)" => 11.3,
"sum (Julia)" => 5.4,
"my_fast_sum (Julia)" => 5.3,
]
bar(first.(results), last.(results), xlabel="function", ylabel="time (ms, shorter is better)", legend=nothing)
With some pretty simple changes, we were able to create a pure-Julia function which is twice as fast as our naive C function while still being clear and completely generic:
my_fast_sum([1, 2.5, π])
6.641592653589793
Just for reference, let's compare with Python. It's easy to call Python code from Julia too--we just need the PyCall
package:
using PyCall
py_math = pyimport("math")
py_math.sin(1.0)
0.8414709848078965
Just as we did with C, we can quickly define a Python sum function without leaving Julia:
# The PyCall package lets us define python functions directly from Julia:
py"""
def mysum(a):
s = 0.0
for x in a:
s = s + x
return s
"""
# mysum_py is a reference to the Python mysum function
py_sum = py"""mysum"""o
PyObject <function mysum at 0x7f2250f9df28>
Let's make sure we're getting similar answers everywhere:
py_sum(data) ≈ c_sum(data) ≈ sum(data) ≈ my_sum(data) ≈ my_fast_sum(data)
true
@btime py_sum($data)
724.217 ms (7 allocations: 352 bytes)
4.998996907719521e6
results = [
"my_sum (Julia)" => 11.7,
"c_sum (C)" => 11.3,
"sum (Julia)" => 5.4,
"my_fast_sum (Julia)" => 5.3,
"py_sum (Python)" => 724.2,
]
bar(first.(results), last.(results), xlabel="function", ylabel="time (ms, shorter is better)", legend=nothing)
double
s in Python than a for
loop.numpy.sum()
is just as fast as Julia's sum()
for large vectors...numpy.sum()
happens to cover the cases you actually need, then go for it!struct Point{T}
x::T
y::T
end
function Base.zero(::Type{Point{T}}) where {T}
Point{T}(zero(T), zero(T))
end
Base.:+(p1::Point, p2::Point) = Point(p1.x + p2.x, p1.y + p2.y)
points = [Point(rand(), rand()) for _ in 1:10^7];
@btime my_fast_sum($points)
15.001 ms (0 allocations: 0 bytes)
Point{Float64}(5.000181181696696e6, 5.000916078448208e6)
@code_native my_fast_sum(points)
.text ; ┌ @ In[64]:2 within `my_fast_sum' movq %rdi, %rax ; │ @ In[64]:8 within `my_fast_sum' ; │┌ @ simdloop.jl:71 within `macro expansion' ; ││┌ @ simdloop.jl:51 within `simd_inner_length' ; │││┌ @ array.jl:221 within `length' movq 8(%rsi), %rcx ; ││└└ ; ││ @ simdloop.jl:72 within `macro expansion' ; ││┌ @ int.jl:49 within `<' testq %rcx, %rcx ; ││└ jle L51 movq (%rsi), %rdx vxorpd %xmm0, %xmm0, %xmm0 nopw %cs:(%rax,%rax) nopl (%rax) ; ││ @ simdloop.jl:77 within `macro expansion' @ In[64]:9 ; ││┌ @ In[75]:10 within `+' @ float.jl:401 L32: vaddpd (%rdx), %xmm0, %xmm0 ; ││└ ; ││ @ simdloop.jl:75 within `macro expansion' ; ││┌ @ int.jl:49 within `<' addq $16, %rdx addq $-1, %rcx ; ││└ jne L32 ; │└ ; │ @ In[64]:11 within `my_fast_sum' vmovupd %xmm0, (%rax) retq L51: vxorps %xmm0, %xmm0, %xmm0 ; │ @ In[64]:11 within `my_fast_sum' vmovups %xmm0, (%rax) retq nopl (%rax) ; └
Julia supports asynchronous cooperative tasks, with libuv
providing the backend. These tasks are great for handling operations like IO or network requests:
using HTTP: request
for i in 1:5
@async begin
println("starting request $i")
r = request("GET", "https://jsonplaceholder.typicode.com/posts/$i")
println("got response $i with status $(r.status)")
end
end
starting request 1 starting request 2 starting request 3 starting request 4 starting request 5 got response 2 with status 200 got response 4 with status 200 got response 3 with status 200 got response 1 with status 200 got response 5 with status 200
Julia also supports parallel and distributed computing (see https://docs.julialang.org/en/v1/manual/parallel-computing/ for more). In addition, Julia 1.3 implemented a new feature, the Parallel Task Run-Time (PATR), which allows for composable multi-threading. It is now possible for a parallelized Julia function to call other parallelized code without over-subsubscribing the available processors. See https://julialang.org/blog/2019/07/multithreading/ for more.
using Base.Threads: @spawn
function fib(n::Int)
if n < 2
return n
end
# `@spawn` creates a new parallel task. Tasks are lightweight and can be
# created at will. The Julia Parallel Task Run-Time handles scheduling the
# tasks to native threads in a depth first manner. That means that you can
# write parallel code which calls other parallel code without over-subscribing
# your available processors.
t = @spawn fib(n - 2)
return fib(n - 1) + fetch(t)
end
fib (generic function with 1 method)
Values are never copied unless you intentionally copy or convert them. That means that functions can mutate their input arguments to efficiently do work in-place:
"""
Invert the sign of the vector `x`, operating in-place to avoid any memory allocation.
"""
function invert!(x::AbstractVector)
for i in eachindex(x)
x[i] = -x[i]
end
end
invert!
Note: the !
in the function name is just a convention: it signals to readers of the code
that the input argument x
will be modified.
x = [1, 2, 3]
invert!(x)
x
3-element Array{Int64,1}: -1 -2 -3
@btime invert!($x)
4.133 ns (0 allocations: 0 bytes)
Julia has no special rules about what can or cannot be assigned to a variable or passed to a function.
A Julia function is a value like any other, so passing functions around and implementing higher-order functions is trivial:
"""
map_reduce: apply `operator` to each element in `array` and reduce pairwise via `reduction`
"""
function map_reduce(operator, reduction, array, initial_value)
result = initial_value
for item in array
result = reduction(result, operator(item))
end
result
end
map_reduce
map_reduce(sin, +, [1, 2, 3, 4], 0)
1.1350859243855171
We can define sum
in terms of map_reduce
:
fancy_sum(x) = map_reduce(identity, +, x, zero(eltype(x)))
fancy_sum (generic function with 1 method)
The performance is just as good as our hand-written sum
loop:
@btime fancy_sum($data)
11.762 ms (0 allocations: 0 bytes)
4.998996907719521e6
To get all the way down to 5ms, we'd need to apply the same @inbounds
and @simd
annotations.
Types can also be passed around as values and bound to variables with no special rules. This makes implementing factories or constructors easy:
function empty_matrix(T::Type, rows::Integer, cols::Integer)
zeros(T, rows, cols)
end
empty_matrix (generic function with 1 method)
empty_matrix(Int, 3, 3)
3×3 Array{Int64,2}: 0 0 0 0 0 0 0 0 0
empty_matrix(Point{Float64}, 3, 3)
3×3 Array{Point{Float64},2}: Point{Float64}(0.0, 0.0) Point{Float64}(0.0, 0.0) Point{Float64}(0.0, 0.0) Point{Float64}(0.0, 0.0) Point{Float64}(0.0, 0.0) Point{Float64}(0.0, 0.0) Point{Float64}(0.0, 0.0) Point{Float64}(0.0, 0.0) Point{Float64}(0.0, 0.0)
Even the expressions that representing Julia code are represented as values in Julia. You can create an expression with the :()
operator, and you can inspect it just like any other object.
expr = :(1 + 2)
:(1 + 2)
An expression has a head
indicating what type of expression it is and zero or more args
:
expr.head
:call
expr.args
3-element Array{Any,1}: :+ 1 2
Since expressions are just values, we can easily write functions to manipulate them:
switch_to_subtraction!(x::Any) = nothing
"""
Change all `+` function calls to `-` function calls.
<sarcasm>
Great for fixing sign errors in your code!
</sarcasm>
"""
function switch_to_subtraction!(ex::Expr)
if ex.head == :call && ex.args[1] == :(+)
ex.args[1] = :(-)
end
for i in 2:length(ex.args)
switch_to_subtraction!(ex.args[i])
end
end
switch_to_subtraction!
expr = :((1 + 2) * (3 + 4) * sqrt(2))
:((1 + 2) * (3 + 4) * sqrt(2))
switch_to_subtraction!(expr)
expr
:((1 - 2) * (3 - 4) * sqrt(2))
A macro is written just like a normal Julia function. The difference is that a macro operates on the expression itself, not on its value:
"""
Modify a given expression, replacing all string literals with "cat"
"""
macro more_cats(expr)
for i in eachindex(expr.args)
if expr.args[i] isa String
expr.args[i] = "cat"
end
end
return esc(expr)
end
@more_cats
Macros are always called with the @
prefix in Julia:
@more_cats println("hello world")
cat
@macroexpand
shows the code that another macro will generate:
@macroexpand @more_cats println("hello world")
:(println("cat"))
@show
: print out the name of a variable and its value. Great for quick debugging:
x = 5
@show x
x = 5
5
@time
measure the elapsed time of an expression and return the result of that expression:
@time sqrt(big(π))
0.000074 seconds (6 allocations: 464 bytes)
1.772453850905516027298167483341145182797549456122387128213807789852911284591025
@showprogress
: Time each iteration of a loop and estimate how much longer it will take to finish:
using ProgressMeter: @showprogress
@showprogress for i in 1:100
sum(rand(10^7))
end
Progress: 100%|█████████████████████████████████████████| Time: 0:00:04
What is the compiler team working on making better? https://discourse.julialang.org/t/compiler-work-priorities/17623
What are some subtle problems that the Julia team would like to fix?
vscode-julia
extension for Visual Studio Code), but they are not as mature as languages like Python, C, Java, etc.https://github.com/julia-vscode/julia-vscode
https://fluxml.ai/Flux.jl/stable/
m = Chain(
Dense(784, 32, σ),
Dense(32, 10), softmax
)
loss(x, y) = Flux.mse(m(x), y)
ps = Flux.params(m)
for i in 1:num_training_iters
Flux.train!(loss, ps, data, opt)
end
https://github.com/SciML/DifferentialEquations.jl
https://github.com/JuliaData/DataFrames.jl
julia> using DataFrames
julia> df = DataFrame(A = 1:4, B = ["M", "F", "F", "M"])
4×2 DataFrame
│ Row │ A │ B │
│ │ Int64 │ String │
├─────┼───────┼────────┤
│ 1 │ 1 │ M │
│ 2 │ 2 │ F │
│ 3 │ 3 │ F │
│ 4 │ 4 │ M │
https://github.com/JuliaOpt/JuMP.jl
Example: Solving a simple model-predictive control problem as a quadratic program (source):
https://www.youtube.com/watch?v=_E2zEzNEy-8
"Overdub" Julia code: