"Print a tree structure that visualizes the type hierarchy rooted at `T`.\n",
"\n",
"# Parameters\n",
"- `T`: The type which serves as the root of the tree to print.\n",
"- `level`: (Optional, default=`0`) An integer specifying the current recursion depth - typically not provided by the user.\n",
"- `max_level`: (Optional, default=`typemax(Int)`) An integer specifying the maximum recursion depth, i.e., how many levels deep into the type hierarchy to print.\n",
"- `prefix`: (Optional, default=`\"\"`) A string used internally to format the tree structure - typically not provided by the user.\n",
"- `subtype_prefix`: (Optional, default=`\"\"`) A string used internally to format the tree structure - typically not provided by the user.\n",
"We can also use this function to print the subtypes of built-in objects."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "53919ada",
"metadata": {},
"outputs": [],
"source": [
"print_type_tree(Number)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1e23272e",
"metadata": {},
"outputs": [],
"source": [
"# Set max_level to 2 to avoid lengthy output.\n",
"print_type_tree(AbstractArray, 0, 2)"
]
},
{
"cell_type": "markdown",
"id": "16d371f7",
"metadata": {},
"source": [
"For concrete structs, constructors are special functions that instantiate objects of a particular type, initiating the lifecycle of an object instance. \n",
"When an object of a custom type is created, it is initialized via constructors, ensuring that it begins its existence in a valid state.\n",
"\n",
"### Default Constructors\n",
"\n",
"The name of a constructor is the same as the name of the type and can have multiple methods, which differ in their argument types. \n",
"By default, Julia provides a constructor that accepts arguments for all fields, unless an `inner constructor` is provided, in which case you need to define all needed constructors yourself.\n",
"For instance, if you have a type named `MyType`, the constructor(s) for this type will also be named `MyType`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "277c436a",
"metadata": {},
"outputs": [],
"source": [
"struct MyType1\n",
" a::Int\n",
" b::Float64\n",
"end\n",
"\n",
"# The default constructor is MyType1\n",
"obj_1 = MyType1(1, 2.0)\n",
"\n",
"@testset \"Test MyType1\" begin\n",
" # Testing with some valid inputs\n",
" obj_1 = MyType1(1, 2.0)\n",
" @test obj_1.a == 1 \n",
" @test obj_1.b == 2.0 \n",
"end"
]
},
{
"cell_type": "markdown",
"id": "2051b0ac",
"metadata": {},
"source": [
"### Inner and Outer Constructors\n",
"\n",
"Julia features inner and outer constructors. Inner constructors are defined inside the block of the type definition and have access to its private internals, while outer constructors are defined outside and call an inner constructor to create an object."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "51d774f8",
"metadata": {},
"outputs": [],
"source": [
"struct MyType2\n",
" a::Int\n",
" b::Float64\n",
" \n",
" # Inner constructor\n",
" function MyType2(a, b)\n",
" a < 0 && throw(ArgumentError(\"a must be non-negative\"))\n",
" new(a, b)\n",
" end\n",
"end\n",
"\n",
"# Outer constructor\n",
"MyType2(a::Int) = MyType2(a, 0.0)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b859da95",
"metadata": {},
"outputs": [],
"source": [
"@testset \"Test MyType2\" begin\n",
" @testset \"Innter constructor with valid inputs\" begin\n",
" obj_21 = MyType2(100, 11.34)\n",
" @test obj_21.a == 100 \n",
" @test obj_21.b == 11.34\n",
" end\n",
" @testset \"Inner constructor with invalid inputs\" begin\n",
" @testset \"Outer constructor with valid inputs\" begin\n",
" obj_22 = MyType2(2)\n",
" @test obj_22.a == 2\n",
" @test obj_22.b == 0.0 \n",
" end\n",
" @testset \"Outer constructor with invalid inputs\" begin\n",
" @test_throws ArgumentError MyType2(-1)\n",
" end\n",
"end"
]
},
{
"cell_type": "markdown",
"id": "e65f0f9a",
"metadata": {},
"source": [
"In the example, the inner constructor validates that `a` is non-negative, providing a measure of control and validation over the initialization process."
]
},
{
"cell_type": "markdown",
"id": "bde87d55",
"metadata": {},
"source": [
"### Parametric Constructors\n",
"\n",
"Parametric types can also have constructors, with parameters specified in the type definition, offering greater flexibility."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9e5c0f17",
"metadata": {},
"outputs": [],
"source": [
"struct MyType3{T1<:Real, T2<:Real}\n",
" a::T1\n",
" b::T2\n",
" \n",
" # Inner constructor\n",
" function MyType3(a::T1, b::T2) where {T1, T2}\n",
" a < 0 && throw(ArgumentError(\"a must be non-negative\"))\n",
" new{T1, T2}(a, b)\n",
" end\n",
"end"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "58e3d497",
"metadata": {},
"outputs": [],
"source": [
"@testset \"Test MyType3\" begin\n",
" @testset \"Valid inputs\" begin\n",
" obj_31 = MyType3(1, 2.0)\n",
" @test obj_31.a == 1 \n",
" @test obj_31.b == 2.0 \n",
" obj_32 = MyType3(1.0, 2.0)\n",
" @test obj_32.a == 1.0\n",
" @test obj_32.b == 2.0 \n",
" obj_33 = MyType3(1, 2)\n",
" @test obj_33.a == 1 \n",
" @test obj_33.b == 2\n",
" end\n",
"end"
]
},
{
"cell_type": "markdown",
"id": "8b60210e",
"metadata": {},
"source": [
"Exercise: `MyType3` can only take a real number. Please write `MyType4` to improve it, using the guidelines below:\n",
"\n",
"- Accept `Number` as inputs.\n",
"- Modify the inner constructor to handle `Complex` numbers for argument `a`. Specifically, if a complex number is supplied, bypass any checking mechanisms.\n",
"- Add an outer constructor like in `MyType3`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4edd8d4e",
"metadata": {},
"outputs": [],
"source": [
"# TODO\n",
"struct MyType4\n",
" a\n",
" b\n",
"end"
]
},
{
"cell_type": "markdown",
"id": "59da2faf",
"metadata": {},
"source": [
"The new MyType4 should pass all of the following tests:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d1cfb791",
"metadata": {},
"outputs": [],
"source": [
"@testset \"Test MyType4\" begin\n",
" @testset \"Inner constructor with valid inputs\" begin\n",
" obj_41 = MyType4(1, 2.0)\n",
" @test obj_41.a == 1 \n",
" @test obj_41.b == 2.0 \n",
" obj_42 = MyType4(1.0, 2.0)\n",
" @test obj_42.a == 1.0\n",
" @test obj_42.b == 2.0 \n",
" obj_43 = MyType4(1, 2)\n",
" @test obj_43.a == 1 \n",
" @test obj_43.b == 2\n",
" obj_44 = MyType4(Complex(1.0, 1.0), 2)\n",
" @test obj_44.a == 1.0 + 1.0im\n",
" @test obj_44.b == 2\n",
" end\n",
" @testset \"Inner constructor with invalid inputs\" begin\n",
" @test_throws ArgumentError MyType4(-1, 1)\n",
" end\n",
" @testset \"Outer constructor with valid inputs\" begin\n",
" @test_throws ArgumentError MyType4(1)\n",
" end\n",
"\n",
" @testset \"Outer constructor with invalid inputs\" begin\n",
" @test_throws ArgumentError MyType4(-1)\n",
" end\n",
"end"
]
},
{
"cell_type": "markdown",
"id": "6bd93a18-6569-4253-a836-0601b77c8bf2",
"metadata": {},
"source": [
"## Dynamical Types"
]
},
{
"cell_type": "markdown",
"id": "c7784fbb",
"metadata": {},
"source": [
"Dynamical types in Julia allow variables to change their type dynamically. \n",
"Julia uses `dynamic typing`, which means that the type of a variable is checked at runtime."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a77d6d3c-e838-45c7-bd71-249c8547281e",
"metadata": {},
"outputs": [],
"source": [
"x = Dog(\"Buddy\", 4) # x is an Dog\n",
"println(typeof(x))\n",
"\n",
"x = Cat(\"Kitty\", 3) # Now x is a Cat\n",
"println(typeof(x))"
]
},
{
"cell_type": "markdown",
"id": "bb3f86ee-ec12-41a4-b29c-a622bc95af8d",
"metadata": {},
"source": [
"## Multiple Dispatch of Functions"
]
},
{
"cell_type": "markdown",
"id": "37ef5d37",
"metadata": {},
"source": [
"Multiple dispatch is one of Julia's key features. \n",
"It allows defining function behavior across many combinations of argument types.\n",
"Different method definitions are dispatched based on the types of all function arguments."
Print a tree structure that visualizes the type hierarchy rooted at `T`.
# Parameters
- `T`: The type which serves as the root of the tree to print.
- `level`: (Optional, default=`0`) An integer specifying the current recursion depth - typically not provided by the user.
- `max_level`: (Optional, default=`typemax(Int)`) An integer specifying the maximum recursion depth, i.e., how many levels deep into the type hierarchy to print.
- `prefix`: (Optional, default=`""`) A string used internally to format the tree structure - typically not provided by the user.
- `subtype_prefix`: (Optional, default=`""`) A string used internally to format the tree structure - typically not provided by the user.
# Usage
```julia
print_type_tree(Number, max_level=7)
```
"""
function print_type_tree(T, level=0, max_level=typemax(Int), prefix="", subtype_prefix="")
We can also use this function to print the subtypes of built-in objects.
%% Cell type:code id:53919ada tags:
``` julia
print_type_tree(Number)
```
%% Cell type:code id:1e23272e tags:
``` julia
# Set max_level to 2 to avoid lengthy output.
print_type_tree(AbstractArray,0,2)
```
%% Cell type:markdown id:16d371f7 tags:
For concrete structs, constructors are special functions that instantiate objects of a particular type, initiating the lifecycle of an object instance.
When an object of a custom type is created, it is initialized via constructors, ensuring that it begins its existence in a valid state.
### Default Constructors
The name of a constructor is the same as the name of the type and can have multiple methods, which differ in their argument types.
By default, Julia provides a constructor that accepts arguments for all fields, unless an `inner constructor` is provided, in which case you need to define all needed constructors yourself.
For instance, if you have a type named `MyType`, the constructor(s) for this type will also be named `MyType`.
%% Cell type:code id:277c436a tags:
``` julia
struct MyType1
a::Int
b::Float64
end
# The default constructor is MyType1
obj_1=MyType1(1,2.0)
@testset"Test MyType1"begin
# Testing with some valid inputs
obj_1=MyType1(1,2.0)
@testobj_1.a==1
@testobj_1.b==2.0
end
```
%% Cell type:markdown id:2051b0ac tags:
### Inner and Outer Constructors
Julia features inner and outer constructors. Inner constructors are defined inside the block of the type definition and have access to its private internals, while outer constructors are defined outside and call an inner constructor to create an object.
%% Cell type:code id:51d774f8 tags:
``` julia
struct MyType2
a::Int
b::Float64
# Inner constructor
function MyType2(a,b)
a<0&&throw(ArgumentError("a must be non-negative"))
new(a,b)
end
end
# Outer constructor
MyType2(a::Int)=MyType2(a,0.0)
```
%% Cell type:code id:b859da95 tags:
``` julia
@testset"Test MyType2"begin
@testset"Innter constructor with valid inputs"begin
obj_21=MyType2(100,11.34)
@testobj_21.a==100
@testobj_21.b==11.34
end
@testset"Inner constructor with invalid inputs"begin
@test_throwsArgumentErrorMyType2(-100,1.0)
end
@testset"Outer constructor with valid inputs"begin
obj_22=MyType2(2)
@testobj_22.a==2
@testobj_22.b==0.0
end
@testset"Outer constructor with invalid inputs"begin
@test_throwsArgumentErrorMyType2(-1)
end
end
```
%% Cell type:markdown id:e65f0f9a tags:
In the example, the inner constructor validates that `a` is non-negative, providing a measure of control and validation over the initialization process.
%% Cell type:markdown id:bde87d55 tags:
### Parametric Constructors
Parametric types can also have constructors, with parameters specified in the type definition, offering greater flexibility.
%% Cell type:code id:9e5c0f17 tags:
``` julia
struct MyType3{T1<:Real,T2<:Real}
a::T1
b::T2
# Inner constructor
function MyType3(a::T1,b::T2)where{T1,T2}
a<0&&throw(ArgumentError("a must be non-negative"))
new{T1,T2}(a,b)
end
end
```
%% Cell type:code id:58e3d497 tags:
``` julia
@testset"Test MyType3"begin
@testset"Valid inputs"begin
obj_31=MyType3(1,2.0)
@testobj_31.a==1
@testobj_31.b==2.0
obj_32=MyType3(1.0,2.0)
@testobj_32.a==1.0
@testobj_32.b==2.0
obj_33=MyType3(1,2)
@testobj_33.a==1
@testobj_33.b==2
end
end
```
%% Cell type:markdown id:8b60210e tags:
Exercise: `MyType3` can only take a real number. Please write `MyType4` to improve it, using the guidelines below:
- Accept `Number` as inputs.
- Modify the inner constructor to handle `Complex` numbers for argument `a`. Specifically, if a complex number is supplied, bypass any checking mechanisms.
- Add an outer constructor like in `MyType3`.
%% Cell type:code id:4edd8d4e tags:
``` julia
# TODO
struct MyType4
a
b
end
```
%% Cell type:markdown id:59da2faf tags:
The new MyType4 should pass all of the following tests:
%% Cell type:code id:d1cfb791 tags:
``` julia
@testset"Test MyType4"begin
@testset"Inner constructor with valid inputs"begin
obj_41=MyType4(1,2.0)
@testobj_41.a==1
@testobj_41.b==2.0
obj_42=MyType4(1.0,2.0)
@testobj_42.a==1.0
@testobj_42.b==2.0
obj_43=MyType4(1,2)
@testobj_43.a==1
@testobj_43.b==2
obj_44=MyType4(Complex(1.0,1.0),2)
@testobj_44.a==1.0+1.0im
@testobj_44.b==2
end
@testset"Inner constructor with invalid inputs"begin
@test_throwsArgumentErrorMyType4(-1,1)
end
@testset"Outer constructor with valid inputs"begin
@test_throwsArgumentErrorMyType4(1)
end
@testset"Outer constructor with invalid inputs"begin