You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/src/lecture_02/lecture.jl
+40-25Lines changed: 40 additions & 25 deletions
Original file line number
Diff line number
Diff line change
@@ -224,14 +224,14 @@ a.x = 2
224
224
# Note, that the memory layout of mutable structures is different, as fields now contain references to memory locations, where the actual values are stored.
225
225
226
226
# ### 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
228
228
struct PositionT{T}
229
229
x::T
230
230
y::T
231
231
end
232
232
u = [PositionT(rand(), rand()) for _ in1:100];
233
233
@btimereduce(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).
235
235
v = [PositionT(rand(1:100), rand(1:100)) for _ in1:100];
236
236
@btimereduce(move, v);
237
237
# 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
258
258
# which means you cannot instantiate them. They purpose is
259
259
# * to allow to define operations for broad class of concrete types
260
260
# * 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.
261
262
#
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
263
264
# ```julia
264
265
# const AbstractVector{T} = AbstractArray{T,1}
265
266
# ```
@@ -275,10 +276,10 @@ end
275
276
276
277
# ## More on use of types in function definitions
277
278
# ### 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
# `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
282
283
move(Position(1,1), Position(2,2))
283
284
move(Position(1.0,1.0), Position(2.0,2.0))
284
285
# 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)
299
300
move(a, by)
300
301
# 1. The compiler knows that you call function `move`
301
302
# 2. and the compiler infers type of arguments (you can see the result using
302
-
(typeof(a),typeof(by))
303
+
(typeof(a),typeof(by))
303
304
# 3. The compiler identifies all methods that can be applied to a function `move` with arguments of type `(Position{Float64}, Position{Float64})`
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
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
307
309
# 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.
308
310
# 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.
# THanks to union splitting, Julia is able to have performant operations on arrays with undefined / missing values for example
333
335
[1, 2, 3, missing] |> typeof
334
336
335
-
### More on matching methods to functions
337
+
# ### More on matching methods and arguments
336
338
# 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].
337
339
#
338
340
# 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?
# 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
381
376
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
383
378
foo(a::Vector{<:Real}) =println("Vector{T} where {T<:Real}")
384
-
385
379
# 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
388
381
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`.
0 commit comments