Skip to content

Commit a2da6ae

Browse files
committed
working on lecture 2
1 parent ae43678 commit a2da6ae

File tree

2 files changed

+160
-99
lines changed

2 files changed

+160
-99
lines changed

docs/src/lecture_02/lecture.jl

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -224,14 +224,14 @@ a.x = 2
224224
# Note, that the memory layout of mutable structures is different, as fields now contain references to memory locations, where the actual values are stored.
225225

226226
# ### Parametric Types
227-
# So far, we had to trade-off flexibility for generality in type definitions. Can we have both? The answer is positive. The way to achieve this **flexibility** in definitions of the type while being able to generate optimal code is to **parametrize** the type definition. This is achieved by replacing types with a parameter (typically a single uppercase character) and decorating in definition by specifying different type in curly brackets. For example
227+
# So far, we had to trade-off flexibility for generality in type definitions. Can we have both? The answer is affirmative. The way to achieve this **flexibility** in definitions of the type while being able to generate optimal code is to **parametrize** the type definition. This is achieved by replacing types with a parameter (typically a single uppercase character) and decorating in definition by specifying different type in curly brackets. For example
228228
struct PositionT{T}
229229
x::T
230230
y::T
231231
end
232232
u = [PositionT(rand(), rand()) for _ in 1:100];
233233
@btime reduce(move, u);
234-
# Notice that the compiler can take advantage of specializing for differen types (which does not have effect as in modern processrs have addition of Float and Int takes the same time).
234+
# Notice that the compiler can take advantage of specializing for different types (which does not have effect as in modern processrs have addition of `Float` and `Int` takes the same time).
235235
v = [PositionT(rand(1:100), rand(1:100)) for _ in 1:100];
236236
@btime reduce(move, v);
237237
# The above definition suffers the same problem as `VaguePosition`, which is that it allows us to instantiate the `PositionT` with non-numeric types, e.g. `String`. We solve this by restricting the types `T` to be childs of some supertype, in this case `Real`
@@ -258,8 +258,9 @@ end
258258
# which means you cannot instantiate them. They purpose is
259259
# * to allow to define operations for broad class of concrete types
260260
# * to inform compiler about constant values, which can be used
261+
# Notice in the above example that parameters of types do not have to be types, but also values of primitive types, as in the above example of `AbstractArray` `N` is the number of dimensions which is an integer value.
261262
#
262-
# For convenience, it is common to name some important partially instantiated Abstract types, for example `AbstractVector` as
263+
# For convenience, it is common to give some important partially instantiated Abstract types an **alias**, for example `AbstractVector` as
263264
# ```julia
264265
# const AbstractVector{T} = AbstractArray{T,1}
265266
# ```
@@ -275,10 +276,10 @@ end
275276

276277
# ## More on use of types in function definitions
277278
# ### Terminology
278-
# * A *function* refers to a set of "methods" for a different combination of type parameters (a term function can be therefore considered as refering to a mere **name**). A *method* defining different behavior for different type of arguments are also called specializations. For example
279+
# * A *function* refers to a set of "methods" for a different combination of type parameters (a term function can be therefore considered as refering to a mere **name**). *Methods* define different behavior for different type of arguments for a given function. For in below example
279280
move(a::Position, b::Position) = Position(a.x + b.x, a.y + b.y)
280281
move(a::Vector{<:Position}, b::Vector{<:Position}) = move.(a,b)
281-
# `move` refers to function, where `move(a, b)`, `move(a::Position, b::Position)` and `move(a::Vector{<:Position}, b::Vector{<:Position})` are methods. When different behavior on different types is defined by a programmer, as shown above, we call it *implementation specialization*. There is another type of specialization, called compiler specialization*, which occurs when the compiler generates different functions for you from a single method. For example for
282+
# `move` refers to a function with methods `move(a::Position, b::Position)` and `move(a::Vector{<:Position}, b::Vector{<:Position})`. When different behavior on different types is defined by a programmer, as shown above, it is also called *implementation specialization*. There is another type of specialization, called compiler specialization*, which occurs when the compiler generates different functions for you from a single method. For example for
282283
move(Position(1,1), Position(2,2))
283284
move(Position(1.0,1.0), Position(2.0,2.0))
284285
# the compiler generates two methods, one for `Position{Int64}` and the other for `Position{Float64}`. Notice that inside generated functions, the compiler needs to use different intrinsic operations, which can be viewed from
@@ -299,11 +300,12 @@ by = Position(2.0, 2.0)
299300
move(a, by)
300301
# 1. The compiler knows that you call function `move`
301302
# 2. and the compiler infers type of arguments (you can see the result using
302-
(typeof(a),typeof(by))
303+
(typeof(a),typeof(by))
303304
# 3. The compiler identifies all methods that can be applied to a function `move` with arguments of type `(Position{Float64}, Position{Float64})`
304-
Base.method_instances(move, (typeof(a), typeof(by)))
305-
m = Base.method_instances(move, (typeof(a), typeof(by))) |> first
306-
# 4a. If the method has been specialized (compiled), which we can check as `Base.isgenerated(m)`, then the arguments are prepared and the method is invoked
305+
Base.method_instances(move, (typeof(a), typeof(by)))
306+
m = Base.method_instances(move, (typeof(a), typeof(by))) |> first
307+
# 4a. If the method has been specialized (compiled), then the arguments are prepared and the method is invoked. The compiled specialization can be seen from
308+
m.cache
307309
# 4b. If the method has not been specialized (compiled), the compiler compiles the method for a given type of arguments and continues as in step 4a.
308310
# A compiled function is therefore a "blob" of **native code** living in a particular memory location. When Julia calls a function, it needs to pick a right block corresponding to a function with particular type of parameters.
309311
#
@@ -332,7 +334,7 @@ wolfpack_c = WolfOrSheep[Wolf("1", 1), Wolf("2", 2), Wolf("3", 3)]
332334
# THanks to union splitting, Julia is able to have performant operations on arrays with undefined / missing values for example
333335
[1, 2, 3, missing] |> typeof
334336

335-
### More on matching methods to functions
337+
# ### More on matching methods and arguments
336338
# In the above process, the step, where Julia looks for a method instance with corresponding parameters can be very confusing. The rest of this lecture will focus on this. For those who want to have a formal background, we recommend (talk of Francesco Zappa Nardelli)[https://www.youtube.com/watch?v=Y95fAipREHQ] and / or the that of (Jan Vitek)[https://www.youtube.com/watch?v=LT4AP7CUMAw].
337339
#
338340
# When Julia needs to specialize a method instance, in needs to find it among multiple definitions. A single function can have many method instances, see for example `methods(+)` which lists all methods instances of `+` function. How Julia select the proper one?
@@ -370,22 +372,35 @@ move([Position(1,2), Position(1.0,2.0)], [Position(1,2), Position(1.0,2.0)])
370372
# 1. Why the following fails?
371373
foo(a::Vector{Real}) = println("Vector{Real}")
372374
foo([1.0,2,3])
373-
# Julia's type system is **invariant**, which means that `Vector{Real}` is different from
374-
# `Vector{Float64}` and from `Vector{Float32}`, even though `Float64` and `Float32` are
375-
# sub-types of `Real`. Therefore `typeof([1.0,2,3])` isa `Vector{Float64}` which is not
376-
# subtype of `Vector{Real}.` For **covariant** languages, this would be true. For more
377-
# information on variance in computer languages, see
378-
# !()[https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)].
379-
# If de above definition of `foo` should be applicable to all vectors which has elements
380-
# of subtype of `Real` we have define it as
375+
# Julia's type system is **invariant**, which means that `Vector{Real}` is different from `Vector{Float64}` and from `Vector{Float32}`, even though `Float64` and `Float32` are sub-types of `Real`. Therefore `typeof([1.0,2,3])` isa `Vector{Float64}` which is not subtype of `Vector{Real}.` For **covariant** languages, this would be true. For more information on variance in computer languages, see ()[https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)]. If de above definition of `foo` should be applicable to all vectors which has elements of subtype of `Real` we have define it as
381376
foo(a::Vector{T}) where {T<:Real} = println("Vector{T} where {T<:Real}")
382-
# or equivalently but more tersely as
377+
# or equivalently but more tersely as
383378
foo(a::Vector{<:Real}) = println("Vector{T} where {T<:Real}")
384-
385379
# 2. Diagonal rule
386-
# rule says that the type repeat in method signature, it has to be
387-
# a concrete type. Consider for example the function below
380+
# rule says that the type repeat in method signature, it has to be a concrete type. Consider for example the function below
388381
move(a::T, b::T) where {T<:Position}
389-
# we cannot call it with `move(Position(1.0,2.0), Position(1,2))`,
390-
# since in this case `Position(1.0,2.0)` is of type `Position{Float64}`
391-
# while `Position(1,2)` is of type `Position{Int64}`.
382+
# we cannot call it with `move(Position(1.0,2.0), Position(1,2))`, since in this case `Position(1.0,2.0)` is of type `Position{Float64}` while `Position(1,2)` is of type `Position{Int64}`.
383+
# 3. When debugging why arguments does not match the particular method definition, it is useful to use `typeof`, `isa`, and `<:` commands. For example
384+
typeof(Position(1.0,2.0))
385+
#
386+
typeof(Position(1,2))
387+
#
388+
Position(1,2) isa Position{Float64}
389+
#
390+
Position(1,2) isa Position{Real}
391+
#
392+
Position(1,2) isa Position{<:Real}
393+
#
394+
typeof(Position(1,2)) <: Position{<:Float64}
395+
#
396+
typeof(Position(1,2)) <: Position{<:Real}
397+
398+
399+
# ### A bizzare definitions which you can encounter
400+
# A following definition of One-Hot Matrix is taken from Flux.jl
401+
struct OneHotArray{T<:Integer, L, N, var"N+1", I<:Union{T,AbstractArray{T, N}}} <: AbstractArray{Bool, var"N+1"}
402+
indices::I
403+
end
404+
# The parameters of the type carry an information about the type used to encode position of `one` in each column in `T`), the dimension of one-hot vectors in `L`, the dimension of the storage of `indices` in `N` (which is zero for OneHotVector and one for OneHotMatrix), number of dimensions of the OneHotArray in `var"N+1"` and the type of underlying storage of indicies `I`.
405+
406+

0 commit comments

Comments
 (0)