Julia Module 6 – The type system and multiple dispatch
Structs, constructors and redefining types (1.12+)
Define immutable and mutable Julia structs, check fields in inner constructors, add outer and keyword constructors and show, and redefine types on 1.12+.
What you will learn
- Define immutable and mutable structs with typed fields, and predict which changes each allows
- Validate field values in an inner constructor, and add outer and keyword constructors around it
- Implement Base.show so a type prints well in the REPL and on one line
- Predict what happens to old values and methods when a struct is redefined on Julia 1.12 and later
Before you start
What you’ll learn · Before you start
What you will learn
- Define immutable and mutable structs with typed fields, and predict which changes each allows
- Validate field values in an inner constructor, and add outer and keyword constructors around it
- Implement Base.show so a type prints well in the REPL and on one line
- Predict what happens to old values and methods when a struct is redefined on Julia 1.12 and later
Before you start
On this page
A four-field struct comes with a constructor that accepts any four values it can convert, so a reading taken at hour
25 with the value NaN and an unknown unit would be built without a word. The mistake then shows up three steps
later, in a mean that came out NaN or a report with an empty row, far from the line that made it.
This lesson defines immutable and mutable structs, writes an inner constructor that makes invalid values impossible to create, adds outer and keyword constructors around it, chooses how values print, and redefines a struct in a running session, which Julia allows from version 1.12. The runs use Julia 1.13.1, and the 1.10 LTS where it differs.
A struct cannot change once it is built, and a mutable struct can
struct declares a composite type: A type with named fields, each with its own type, declared with struct or mutable struct, such as struct Point x::Float64; y::Float64 end. Other languages call it a record or an object type.: a type with named fields, each with a type.
Values of it are immutable struct: A composite type declared with struct: its fields cannot be assigned once the value is built, two values with equal fields are equal, and a struct of plain data is stored inline in arrays., and mutable struct declares one whose fields can be
assigned. Two small types and a third that mixes the two:
# An immutable struct and a mutable one, and what each lets you change.
struct Point
x::Float64
y::Float64
end
println(methods(Point)) # the two default constructors Julia wrote
p = Point(1, 2) # the second one converts 1 and 2 to Float64
println(p, " isbits: ", isbits(p), ", sizeof: ", sizeof(p), " bytes")
try
p.x = 5.0
catch err
println(sprint(showerror, err))
end
mutable struct Counter
n::Int
end
c = Counter(0)
d = c # a second name for the same object
d.n += 1
println("c.n = ", c.n, ", c === d: ", c === d, ", ismutable(c): ", ismutable(c))
# == compares the fields of immutable values, but only the identity of mutable objects.
println("Point(1, 2) == Point(1, 2): ", Point(1, 2) == Point(1, 2))
println("Counter(1) == Counter(1): ", Counter(1) == Counter(1))
# An immutable struct can hold a mutable vector: the field cannot change, its contents can.
struct Batch
name::String
values::Vector{Float64}
end
b = Batch("run 1", [1.0, 2.0])
push!(b.values, 3.0)
println(b)
println(fieldnames(Batch), " ", fieldtypes(Batch), " fieldindex(Batch, :values) = ", fieldindex(Batch, :values)) from session import files, run
# Two structs, one immutable and one mutable, and a third that holds a vector.
files("fields.jl")
run("""
julia fields.jl
""") Output
$ julia fields.jl
# 2 methods for type constructor:
[1] Point(x::Float64, y::Float64)
@ ~/demo/fields.jl:3
[2] Point(x, y)
@ ~/demo/fields.jl:3
Point(1.0, 2.0) isbits: true, sizeof: 16 bytes
setfield!: immutable struct of type Point cannot be changed
c.n = 1, c === d: true, ismutable(c): true
Point(1, 2) == Point(1, 2): true
Counter(1) == Counter(1): false
Batch("run 1", [1.0, 2.0, 3.0])
(:name, :values) (String, Vector{Float64}) fieldindex(Batch, :values) = 2
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fields_session.py
Julia wrote two constructors for Point by itself, Point(x::Float64, y::Float64) and Point(x, y), which converts
its arguments, so Point(1, 2) stored 1.0 and 2.0. Step through what the names in the program hold:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- An immutable value. Point is a struct, so p is a value made of its two Float64 fields. It holds no references, so isbits(p) is true and the value takes 16 bytes.
- A field cannot change. p.x = 5.0 stopped with "setfield!: immutable struct of type Point cannot be changed". To change a point, build a new one: p = Point(5, 2).
- A mutable object. Counter is a mutable struct, so Counter(0) is an object with an identity of its own, and c refers to it.
- Two names, one object. d = c copies nothing: d refers to the same object. d.n += 1 changed it, so c.n printed 1 and c === d was true.
- What == compares. Point(1, 2) == Point(1, 2) was true: equal fields make equal values. Counter(1) == Counter(1) was false: by default, two mutable objects are equal only if they are the same object.
A Point is plain data: isbits(p) was true and the value takes 16 bytes, so a Vector{Point} stores the points
side by side, with no pointers. Assigning a field stopped with
setfield!: immutable struct of type Point cannot be changed. A Counter is an object with an identity: d = c gave the same object a second name, so a change through
d showed through c. The manual says a mutable object can only be identified reliably by its address, which is why
Counter(1) == Counter(1) was false while Point(1, 2) == Point(1, 2) was true: == falls back to ===, which
compares the fields of immutable values and the identity of mutable ones.
Batch shows the limit of immutability. Its fields cannot be made to point elsewhere, but its values field is a
Vector, and push!(b.values, 3.0) changed the vector’s contents. fieldnames, fieldtypes and fieldindex
describe a type’s fields; fieldindex, which gives a field’s position, is exported from Julia 1.13 (before that it
is written Base.fieldindex).
Comparing mutable records with ==
Wrong: mutable struct Config … end and then Config(5) == Config(5) in a test, which is false because ==
compares identity. Right: make the type immutable when its fields need not change, or define
Base.:(==)(a::Config, b::Config) (and hash, so that dictionaries agree) to compare the fields.
Checkpoint
Check: Immutable and mutable structs
An inner constructor is the only way in, so it can check every field
An inner constructor: A constructor written inside the struct block. Only it may call new, which builds the value, so checks written there hold for every value of the type; defining one removes the default constructors. is a constructor written inside the struct block. It is the
only code that may call new, the function that actually builds the value, and the manual adds that once any inner
constructor is defined, Julia provides no default one. So the checks it makes hold for every value of the type:
# A reading that cannot be built with invalid values: the inner constructor checks every field.
const UNITS = ("°C", "mm", "hPa")
struct Reading
station::String
hour::Float64
value::Float64
unit::String
function Reading(station::AbstractString, hour::Real, value::Real, unit::AbstractString)
isempty(station) && throw(ArgumentError("station: must not be empty"))
0 <= hour < 24 || throw(ArgumentError("hour: must be in [0, 24), got $hour"))
isnan(value) && throw(ArgumentError("value: NaN is not a reading"))
unit in UNITS || throw(ArgumentError("unit: \"$unit\" is not one of $(join(UNITS, ", "))"))
return new(station, hour, value, unit)
end
end
# An outer constructor: a convenient form that calls the inner one, so the checks still run.
Reading(station::AbstractString, value::Real, unit::AbstractString) = Reading(station, 12, value, unit)
println(Reading("Pune", 9.5, 24.5, "°C"))
println(Reading("Pune", 12.3, "mm"))
for args in (("", 9, 24.5, "°C"), ("Pune", 25, 24.5, "°C"), ("Pune", 9, NaN, "°C"), ("Pune", 9, 24.5, "F"))
try
Reading(args...)
catch err
println(sprint(showerror, err))
end
end
println(methods(Reading)) # the inner and the outer constructor, and no default one from session import files, run
# The checks of an inner constructor, and an outer constructor that goes through them.
files("reading.jl")
run("""
julia reading.jl
""") Output
$ julia reading.jl
Reading("Pune", 9.5, 24.5, "°C")
Reading("Pune", 12.0, 12.3, "mm")
ArgumentError: station: must not be empty
ArgumentError: hour: must be in [0, 24), got 25
ArgumentError: value: NaN is not a reading
ArgumentError: unit: "F" is not one of °C, mm, hPa
# 2 methods for type constructor:
[1] Reading(station::AbstractString, value::Real, unit::AbstractString)
@ ~/demo/reading.jl:18
[2] Reading(station::AbstractString, hour::Real, value::Real, unit::AbstractString)
@ ~/demo/reading.jl:9
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reading_session.py
Each check throws an ArgumentError whose message starts with the field it is about, so the error says what to fix:
an empty station, hour 25, a NaN value and the unit "F" were each refused. The method table at the end lists two
constructors and no default one, which is the point: there is no way to build a Reading that skips the checks.
Write the check so that NaN fails it
<, <=, >, >= and == are all false when one side is NaN, so lo <= hi || throw(…) refuses NaN as
well, and lo > hi && throw(…) lets it through. When a value must be a number, test isnan by name, as Reading does, so the message says what
happened.
Step through one call of the shorter form:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The call. Reading("Pune", 12.3, "mm") has three arguments. No method of the inner constructor takes three, so the outer constructor runs.
- The outer constructor. It fills in the hour, 12, and calls Reading with four arguments: the inner constructor, the only method that can build a Reading.
- The checks. The station is not empty, 12 is in [0, 24), 12.3 is not NaN and "mm" is a known unit, so every check passes.
- new. new(station, hour, value, unit) converts each argument to its field type, so the hour 12 is stored as 12.0, and builds the value.
- The result. The run printed Reading("Pune", 12.0, 12.3, "mm"). Every Reading that exists has passed the same four checks.
- A failed check. Reading("Pune", 9, NaN, "°C") fails the value check, so new never runs and no Reading is made: the run printed "ArgumentError: value: NaN is not a reading".
The three-argument form is an outer constructor: A constructor written outside the struct block, as an ordinary method that calls another constructor. It adds convenient forms, such as defaults, while the inner constructor's checks still run.: an ordinary method that fills in
the hour and calls the inner constructor, so the checks run for it too. new converts each argument to its field
type, which is why the hour 12 was stored as 12.0.
Key idea
Put the rules a value must obey in its inner constructor: every way of building the type ends in that constructor’s
new, so no value of the type can break them.
Checkpoint
Check: Checking fields in an inner constructor
Outer and keyword constructors add convenient ways in
The manual’s advice is to write as few inner constructors as possible, one that takes every field and checks it, and
to add every convenient form outside, as plain methods that call it. Reading("Pune", 12.3, "mm") was such a form.
For a type with many settings and sensible defaults, Base.@kwdef writes a keyword constructor:
# Solver options with defaults: Base.@kwdef writes a keyword constructor for the struct.
Base.@kwdef struct SolverOptions
maxiter::Int = 100
tol::Float64 = 1e-8
verbose::Bool = false
name::String # no default: a required keyword
end
# Bisection on f over [a, b], stopped by the options.
function solve(f, a, b, opts::SolverOptions)
for k in 1:opts.maxiter
m = (a + b) / 2
opts.verbose && println(" step ", k, ": m = ", m)
(b - a) / 2 < opts.tol && return (root = m, steps = k)
sign(f(m)) == sign(f(a)) ? (a = m) : (b = m)
end
return (root = (a + b) / 2, steps = opts.maxiter)
end
f(x) = x^2 - 2
println(SolverOptions(name = "default"))
println(solve(f, 1.0, 2.0, SolverOptions(name = "default")))
println(solve(f, 1.0, 2.0, SolverOptions(name = "coarse", tol = 0.02, verbose = true)))
println(SolverOptions(100, 1e-8, false, "fast")) # the positional constructor is still there
for call in (() -> SolverOptions(), () -> SolverOptions(name = "x", maxiter = 2.5))
try
call()
catch err
println(sprint(showerror, err))
end
end from session import files, run
# A struct of options with keyword defaults, passed to a small solver.
files("kwdef_solver.jl")
run("""
julia kwdef_solver.jl
""") Output
$ julia kwdef_solver.jl SolverOptions(100, 1.0e-8, false, "default") (root = 1.4142135605216026, steps = 27) step 1: m = 1.5 step 2: m = 1.25 step 3: m = 1.375 step 4: m = 1.4375 step 5: m = 1.40625 step 6: m = 1.421875 (root = 1.421875, steps = 6) SolverOptions(100, 1.0e-8, false, "fast") UndefKeywordError: keyword argument `name` not assigned InexactError: Int64(2.5)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 kwdef_solver_session.py
SolverOptions(name = "default") took every other field from its default: 100 iterations, a tolerance of 1e-8 and
no printing, and the bisection stopped at step 27, at 1.4142135605216026. The second call set a tolerance of
0.02 and switched printing on, leaving maxiter at 100, so it stopped after 6 steps and printed each one. A field
without a default is a required keyword, as the @kwdef documentation says, so SolverOptions() stopped with
UndefKeywordError: keyword argument `name` not assigned. The keyword constructor converts like new does, and
maxiter = 2.5 failed with InexactError: Int64(2.5). The positional constructor still exists, and
SolverOptions(100, 1e-8, false, "fast") built a value too.
Settings as loose keyword arguments everywhere
Wrong: solve(f, a, b; maxiter = 100, tol = 1e-8, verbose = false), with the same keywords and defaults repeated in
every function that passes settings on, so a wrapper that forgets to pass tol along silently uses its own default.
Right: one @kwdef struct of options, built once and passed as a single typed argument, as solve(f, a, b, opts)
takes it.
Checkpoint
Check: Outer and keyword constructors
Two show methods decide how a value prints
By default a value prints as its constructor call, Interval(1.0, 2.5): a valid Julia expression, which the
manual’s rule of thumb asks of the one-line form, so it can be pasted back. Julia uses two show methods, and you
may define either:
# How values print: the two-argument show gives the one-line form, the three-argument one the REPL's display.
struct Interval
lo::Float64
hi::Float64
end
iv = Interval(1, 2.5)
println("default: ", iv) # a valid Julia expression, as the manual recommends
# A display for people, used by the REPL and display(); the one-line form stays as it was.
function Base.show(io::IO, ::MIME"text/plain", x::Interval)
print(io, "Interval from ", x.lo, " to ", x.hi, ", width ", x.hi - x.lo)
end
println("println: ", iv, " string: ", string(iv), " in a vector: ", [iv])
print("display: ")
display(iv)
# A struct with a long field: its default one-line form prints every value.
struct Series
name::String
values::Vector{Float64}
end
s = Series("rain", collect(0.0:0.5:499.5))
println("default one-line form: ", length(repr(s)), " characters")
Base.show(io::IO, x::Series) = print(io, "Series(", repr(x.name), ", ", length(x.values), " values)")
println("custom one-line form: ", repr(s), ", ", length(repr(s)), " characters")
println("in a vector: ", [s, Series("temp", [24.5, 26.0])]) from session import files, run
# The same Interval printed before and after two show methods.
files("show.jl")
run("""
julia show.jl
""") Output
$ julia show.jl
default: Interval(1.0, 2.5)
println: Interval(1.0, 2.5) string: Interval(1.0, 2.5) in a vector: Interval[Interval(1.0, 2.5)]
display: Interval from 1.0 to 2.5, width 1.5
default one-line form: 6796 characters
custom one-line form: Series("rain", 1000 values), 27 characters
in a vector: Series[Series("rain", 1000 values), Series("temp", 2 values)]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 show_session.py
| Method | Used by | Rule |
|---|---|---|
show(io::IO, x) |
print, println, string, repr, and values inside an array |
One line; a valid expression where possible |
show(io::IO, ::MIME"text/plain", x) |
display, and the REPL when it shows a result |
Any layout, for people; falls back to the first |
The three-argument method changed only what display printed, Interval from 1.0 to 2.5, width 1.5, while
println, string and the vector kept the one-line form. Override the one-line form when the default cannot stay
short: Series holds 1,000 values, its default one-line form was 6,796 characters long, and a two-argument show
brought it to 27, Series("rain", 1000 values), which is also how it now appears inside a vector.
Print through io, never to the screen
A show method writes to the io it is given, with print(io, …). A method that calls println(…) without io
prints in the wrong place and breaks string(x), repr(x) and every array that holds the value.
Checkpoint
Check: The two show methods
Julia 1.12 can redefine a struct, and old values keep the old type
Until Julia 1.12, changing a struct’s fields meant restarting the session. The 1.12 release notes made redefinitions of constants follow the world age: A counter that grows with every change to Julia's methods and bindings. Running code sees the definitions of the world it started in, so a new method reaches it only after it returns or through invokelatest. More in “Notebooks, editors and the Revise workflow” rules and allowed other redefinitions, such as of types. The lesson on notebooks and Revise showed that the second definition is accepted; this run follows what happens to a value and a method made before it:
# A struct redefined in a running session, with a method written for the first definition.
struct Sample
value::Float64
end
describe(s::Sample) = "sample of $(s.value)"
old = Sample(1.0)
println(describe(old))
struct Sample # the second definition adds a field
value::Float64
unit::String
end
new = Sample(2.0, "mV")
println(old, " ", new)
println("typeof(old) == Sample: ", typeof(old) == Sample, ", typeof(new) == Sample: ", typeof(new) == Sample)
try
describe(new)
catch err
println(first(split(sprint(showerror, err), '\n')))
end
println(describe(old)) # the old method still works on the old value
describe(s::Sample) = "sample of $(s.value) $(s.unit)"
println(describe(new))
struct Sample # the same definition again creates no new type
value::Float64
unit::String
end
println("after an identical definition, typeof(new) == Sample: ", typeof(new) == Sample) from session import files, run, use
# The same file on Julia 1.13.1, then on the 1.10 LTS.
files("redefine_sample.jl")
run("""
julia --version
julia redefine_sample.jl
""")
use("1.10.12")
run("""
julia --version
!julia redefine_sample.jl
""") Output
$ julia --version julia version 1.13.1 $ julia redefine_sample.jl sample of 1.0 @world(Sample, 39148:39153)(1.0) Sample(2.0, "mV") typeof(old) == Sample: false, typeof(new) == Sample: true MethodError: no method matching describe(::Sample) sample of 1.0 sample of 2.0 mV after an identical definition, typeof(new) == Sample: true $ julia --version julia version 1.10.12 $ julia redefine_sample.jl sample of 1.0 ERROR: LoadError: invalid redefinition of constant Main.Sample Stacktrace: [1] top-level scope @ ~/demo/redefine_sample.jl:9 in expression starting at ~/demo/redefine_sample.jl:9 [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 redefine_sample_session.py
Step down the session and watch which definition each value and method belongs to:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The first type. The first struct Sample has one field. describe(s::Sample) is written for it, and describe(old) printed "sample of 1.0".
- A new type, same name. Julia 1.12 and later accept the second definition. Sample now names a new type with two fields; the old type still exists, under a name that says which world it belongs to.
- The old value. old keeps the type it was made with. It printed as @world(Sample, 39148:39153)(1.0), and typeof(old) == Sample was false.
- A value of the new type. Sample(2.0, "mV") builds a value of the new type, and typeof(new) == Sample is true.
- The old method. describe was written for the old type, so describe(new) stopped with "MethodError: no method matching describe(::Sample)", while describe(old) still printed "sample of 1.0".
- Methods follow by hand. Defining describe again, after the new struct, made describe(new) print "sample of 2.0 mV". Every method written for the old type has to be defined again this way.
The name Sample moved to the new type, and nothing else moved with it. old printed as
@world(Sample, 39148:39153)(1.0), the old type named by the range of worlds in which Sample meant it, and
typeof(old) == Sample was false. The method describe(s::Sample) was written while Sample meant the old type,
and its signature still names that type, so describe(new) threw a MethodError until describe was defined again; the 1.12 highlights warn that functions need
to be redefined to work on the new struct. A definition identical to the current one makes no new type:
typeof(new) == Sample stayed true after it.
Struct redefinition needs Julia 1.12 or later
On the 1.10.12 LTS the same file stopped at its second struct Sample with
invalid redefinition of constant Main.Sample and exit status 1. Code that must also run there keeps each struct
definition in one place and changes it only between sessions.
In practice, after you change a struct, run again the file that defines its methods and rebuild the values you still need; values left from before keep the old type and the old methods. Keep long-lived data in plain containers, such as a vector of named tuples, while the struct’s design is still changing.
Checkpoint
Check: Redefining a struct on Julia 1.12 and later
Practise: readings and a calibration
A validated reading and a mutable calibration
Extend Reading so its inner constructor also rejects infinite values, with a message that names the field. Then
write mutable struct Calibration with offset and gain, and apply!(cal, r), which returns a new Reading
with the value cal.gain * r.value + cal.offset and counts how many readings it has corrected in a field of cal.
Check on your machine that Reading("Pune", 9, Inf, "°C") is refused, that apply! leaves its argument unchanged,
and that the count grows by one with each call.
Next comes the self-check on fields, constructors, printing and redefinition. Questions on your weakest idea come first, and a wrong choice says which mistake it stands for:
Practice that adapts to you
Practise this lesson
12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): when would you choose a mutable struct over a struct in Julia?
Choose struct by default. An immutable value is defined by its fields alone: == compares them, it can be stored
inline in arrays when its fields are plain data, as a 16-byte Point was, and no other part of the program can
change it behind your back. Choose mutable struct when the object has an identity that persists while its state
changes, such as a counter, a simulation’s running state or a buffer that is updated in place many times, so that
building a new value for every change would cost too much. Two mutable objects with equal fields are not == unless
you define it, and every name bound to one sees its changes. An immutable struct may still hold a mutable vector,
whose contents can change.
Two design questions grow out of this lesson: a record type that keeps letting bad data in, and a long session in which a struct was changed. Sketch the type you would write for the first and the routine you would follow for the second, then compare them with the first steps:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to senior
Sensor readings are stored in a struct with the fields station, hour, value and unit. Analysts keep finding NaN values, hours above 24 and unknown units in the results. Redesign the type so that such readings cannot exist, and explain what changes for the code that builds them.
-
Mid to staff
During a long Julia 1.12 session you add a unit field to a struct. Afterwards a function that worked before throws a MethodError for new values, and values in a cache print as @world(Sample, …). Explain what happened, and how you would work with struct redefinition safely.
Key takeaways
structvalues cannot change after they are built and compare by their fields;mutable structobjects can, and compare by identity unless==is defined.- An immutable struct of plain data is stored inline, 16 bytes for a
Pointof twoFloat64fields, and a field that holds a vector still has contents that can change. - An inner constructor is the only way to call
new, so its checks hold for every value, and defining one removes the default constructors. - Outer constructors and
Base.@kwdefkeyword constructors add convenient forms that still go through the inner constructor; a field without a default is a required keyword. show(io, x)is the one-line form used byprint,stringand arrays, andshow(io, ::MIME"text/plain", x)the display the REPL uses.- Julia 1.12 and later accept a changed struct definition in the same session: values made before keep the old type, methods are defined again by hand, and the 1.10 LTS refuses the second definition.
Terms in this lesson
- composite type
- A type with named fields, each with its own type, declared with struct or mutable struct, such as struct Point x::Float64; y::Float64 end. Other languages call it a record or an object type.
- immutable struct
- A composite type declared with struct: its fields cannot be assigned once the value is built, two values with equal fields are equal, and a struct of plain data is stored inline in arrays.
- inner constructor
- A constructor written inside the struct block. Only it may call new, which builds the value, so checks written there hold for every value of the type; defining one removes the default constructors.
- outer constructor
- A constructor written outside the struct block, as an ordinary method that calls another constructor. It adds convenient forms, such as defaults, while the inner constructor's checks still run.
References
- Types: composite types, mutable composite types and custom pretty-printing (The Julia Project)
- Constructors: inner and outer constructor methods (The Julia Project)
- Essentials: Base.@kwdef, fieldnames, fieldtypes, fieldindex and ismutable (The Julia Project)
- The world age mechanism (The Julia Project)
- Julia 1.12 highlights: redefinition of constants and structs (The Julia Project)
- Julia 1.12 release notes: redefinition of constants and types (The Julia Project)
- Julia 1.13 release notes: fieldindex is exported (The Julia Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress