← Back to list

10 Random Julia Tips To Help You On Your Way

10 of my favorite code snippets to completely change your Julia software.

Emma Boudreau in chifi · 2026-08-04 06:32 · 154 claps · 14.0 min read paywalled
#programming #julia #julialang #data-science #computer-science
Open on Medium ↗
Wiki topics: ML · Machine Learning 💻 · Programming 🔬 · Science · General 🐾 · Pets & Animals

10 Random Julia Tips To Help You On Your Way

10 of my favorite code snippets to completely change your Julia software.

Julia is awesome

Working in Julia is a unique experience. The language is incredibly feature-rich and operates inside its own unique programming paradigm built around the multiple dispatch concept. On top of being faster than many of its contemporaries in the statistical language space, Julia also features macros, parameters, polymorphism, abstraction, robust parallel computing support, and a plethora of other features primarily facilitated through multiple dispatch.

All of the big features I just mentioned are great; they open the door for an infinite range of possibilities in designing a software project. Despite how comprehensive the feature set is, it really only scratches the surface of what Julia has to offer. Beyond the language’s methodology, Julia also has a pretty fantastic and amazingly capable Base module and Standard Library. Because there are so many small components to base Julia, it is hard to cover them all and even harder to learn them all. To make the language easier to digest, it is a great idea to take in a few topics at a time and learn more about the language in small, bite-sized chunks.

  • Perhaps 10 bite-sized chunks.

notebook for this project

№1: SubStrings

Standard string slicing in Julia and most other languages, using range indexing (text[1:5]for example) creates a brand-new string in memory by making a full copy of those characters. This exacerbates Julia’s already prominent memory-based shortcomings, and also hits performance pretty hard if we are slicing and editing strings in many places. We can create a Substring by calling the SubString constructor or the @view macro.

  • For the SubString constructor, we provide our AbstractString along with the start index and finishing index of our slice. These can be provided directly as integers or as a UnitRange{Integer}
substr = SubString("hello", 3, 4)
substr = SubString("hello", 3:4)
  • For the @view macro, we index as we normally would, but we provide the @view macro before we index.
substr = @view "hello"[3:4]

Here is a deeper example:


text = "Julia is awesome"

sub1 = SubString(text, 1, 5) 
sub2 = @view text[10:end]   

println(sub1)  # Output: Julia
println(sub2)  # Output: awesome

# 3. Checking types
println(typeof(text))  # String
println(typeof(sub1))  # SubString{String}
println(typeof(sub2))  # SubString{String}

A SubString is a sub-type of AbstractString , so it will behave almost exactly like a normal String and the vast majority of functions will work just fine — one of the fantastic byproducts of multiple dispatch. If we ever absolutely need a real String for whatever reason, we can allocate a SubString to a new String by simply calling string on it.

println(typeof(string(sub1)))  # String

All of this adds up to making SubStrings an absolute no-brainer. Using string views instead of allocating new strings can provide huge gains in performance.

№2: Extending Base.vect

I don’t know if extending Base.vect is necessarily the best practice, as it overwrites the Method responsible for vectorizing that type — this could have unintended consequences in other places when other packages try to put that structure into a Vector. However, it is still a cool thing you can do in Julia. Not just that, but I have thought of potential use-cases for this. For example, in Data Science Base.vect might make some sort of modeling pipeline.

import Base.vect
abstract type AbstractChain end
abstract type AbstractModel <: AbstractChain end
abstract type AbstractOperation <: AbstractChain end

struct Operation <: AbstractOperation

end

struct SampleModel <: AbstractModel

end

mutable struct ModelPipeline{T <: AbstractModel}
    preprocessing::Vector{Operation}
    model::T
    postprocessing::Vector{Operation}
end

function vect(parts::AbstractChain ...)
    pre::Bool = true
    preprocessing::Vector{Operation} = Vector{Operation}()
    post::Vector{Operation} = Vector{Operation}()
    model = nothing
    for part in parts
        T = typeof(part)
        if T <: AbstractModel
           model = part
           pre = false
           continue
        end
        if T <: AbstractOperation
            if pre
              push!(preprocessing, part)
              continue
            end
            push!(post, part)
        end
    end
    if isnothing(model)
        model = SampleModel()
    end
    ModelPipeline{typeof(model)}(preprocessing, model, post)
end

Of course, there is no reason that you can’t just make a function called pipeline instead of tying this to vect , but it certainly is cool that you could if you wanted to. There could be more specific settings where extending vect is necessary or adds simplicity to a project.

op1 = Operation()
op2 = Operation()
[op1, op2]

Regardless as to whether or not you should extend this function, it is really cool how Julia presents possibilities like this through the paradigm of multiple dispatch.

№3: Detailed sorting with Base.sort

Julia’s Base.sort and Base.sort! offer a lot of flexibility beyond just simple ascending order. Through standard keyword arguments (by, lt, rev, alg, and order), Base.sort handles multi-level sorting, custom criteria transformation, and custom comparison rules without requiring custom structures or boilerplate algorithms.

The first keyword argument to remember is by . This argument allows us to provide a Function and use that function’s return to sort our values. For example, sorting by length .

sort(["hi", "hello", "bye"], by = length)

The rev argument is also pretty easy to use, we provide rev = true if we want to reverse our sort:

sort(["hi", "hello", "bye"], by = length, rev = true)

The lt argument allows us to do more advanced sorting by comparing values using a provided function. In the following example we sort the numbers by how close they are to 10:

target = 10
nums = [3, 12, 8, 32, 60]

sort(nums, lt = (a, b) -> abs(a - target) < abs(b - target))

Finally, alg allows us to choose a sorting algorithm. Julia picks sensible algorithm defaults based on data type, but you can explicitly specify the algorithm to tune performance, memory usage, or stability:

  • QuickSort is fast, in-place, but unstable. It is ideal for general numeric data.
  • MergeSort is an O(n) algorithm, generally takes more memory and performance, but is stable. So if you need to preserve the order of equal keys, we would use MergeSort over QuickSort
  • InsertionSort is specifically used for small collections, generally under 20 elements, and generally can sort these small collections really fast. It is also stable.
  • Finally, RadixSort is a linear, non-comparative sorting method . Unlike comparison-based algorithms like Merge Sort or Quick Sort, Radix Sort never directly compares two values against each other. Instead, it temporarily stores a new array and sorts the values into it using assigned keys and saved positions — usually using a Least Significant Digit (LSD) system.

It is important to select the correct sorting algorithm, particularly for software that needs to sort consistently. The best sorting algorithm will always depend on your use-case, but generally I say QuickSort and MergeSort work for most cases:

people = [["steve", 26, 5], ["gwinett", 19, 2], ["nicole", 35, 4]]
# Sort without preserving equal-element ordering
sort(people, by=x -> x[2], alg=QuickSort)

# Explicitly preserve equal-element ordering with a stable sort
sort(people, by=x -> x[2], alg=MergeSort)
3-element Vector{Vector{Any}}:
 ["gwinett", 19, 2]
 ["steve", 26, 5]
 ["nicole", 35, 4]

№4: GC.gc

One of the most annoying things about Julia is managing memory. The language itself takes around 80MB, and the multiple dispatch concept doesn’t do the language any favors once we start loading more code. The language prioritizes speed over memory usage, primarily because Julia is primarily intended for use at runtime. Despite this specialization, Julia still has some pretty diverse capabilities that extend far beyond runtime and into live-service applications and features.

Given that memory is one of the larger constraints in the language, we want to be managing memory effectively and cleaning up data when we don’t need to use it anymore. Unfortunately, Julia isn’t as simple as Python in this regard — there is no equivalent to del that exists in Julia. Instead, garbage collection is almost entirely implicit and done by the compiler itself. To give credit where it is due, in my experience Julia’s garbage collection system works pretty well, but you’ll notice memory generally frees whenever it wants to. In most cases you’ll have to set things declared in Main to nothing to get rid of them. And even then, this will only happen reliably right away if we follow our nothing assertions with a call to a special gc function. This function is inside the GC Module , which is part of Base . We can access it from Main by calling getfield on GC :

GC.gc()

We can also provide the argument full , as a positional argument, this will do a full GC flush, but it should be noted that this takes a while. These calls aren’t necessarily required for every server, runtime, or application, but in specific cases this might be used to explicitly call for garbage collection in Julia.

It is also worth mentioning that there is another well known gc function that often gets confused for GC.gc . This is Pkg.gc , which serves a similar but different purpose, performing garbage collection for the Pkg package manager. This is more of a long-term function that is meant to remove inactive, or “orphaned,” packages and artifacts from your system. These will only remove inactive objects that remain unused for a default period of 7 days. This function is less useful in the context of actual projects, and isn’t particularly noteworthy because it is called automatically by Pkg .

From the Pkg Documentation: To disable automatic garbage collection, you can set the environment variable JULIA_PKG_GC_AUTO to “false” before starting Julia or call API.auto_gc(false).

№5: In-Place Functions and the ! Convention

It is important to grasp the concept of ‘in-place’ or ‘mutating’ functions. These are functions that mutate their arguments in-place, rather than providing a return. In Julia there is a convention where these functions always end with an exclamation point. This convention is incredibly important because it allows developers to quickly assess whether or not a function will mutate their value. Compare the regular get_index function to the set_index! in-place function:

struct Vector3
    x::Int64
    y::Int64
    z::Int64
end

function get_index(vec::Vector3, n::Integer)
    (vec.x, vec.y, vec.z)[n]
end

function set_index!(vec::Vector3, n::Integer, value::Integer)
    vals = (:x, :y, :z)
    setfield!(vec, vals[n], value)
end

The ! convention works wonders here, as it allows us to use the correct version of the function easily and control our data effectively. When most functions have a mutating and non-mutating version, we can easily choose when we want to create a new copy of an object or save allocations by working with the object itself. Sometimes we will want to use filter and sometimes we will want to use filter! . It is great to have access to both, and it is even better to easily distinguish them and remember their names. Because Julia standardizes this convention, the functions for the vast majority of packages use it. This is a serious improvement over alternatives like learning normalize and normalize_inplace or some other strange naming scheme.

№6: Zero-Allocation Slices with @views

Standard array slicing in Julia creates a copy of the underlying memory. If you perform multiple slice operations inside a loop or function, performance plummets due to continuous memory allocations and garbage collection overhead. To mitigate this, Julia provides SubArrays (views) via the @view macro or @views block macro, which reference existing memory instead of allocating a copy:

# Copies memory (creates temporary arrays)
function sum_matrix_columns_bad(M::Matrix{Float64})
    total = 0.0
    for col in 1:size(M, 2)
        total += sum(M[:, col])  # M[:, col] allocates a new vector each iteration!
    end
    return total
end

function sum_matrix_columns_good(M::Matrix{Float64})
    total = 0.0
    @views for col in 1:size(M, 2)
        total += sum(M[:, col])  # Creates a SubArray view instead of allocating
    end
    return total
end

M = rand(2000, 2000)

№7: Replacing If with multiple dispatch

The following code may be considered perfectly acceptable or normal in many programming languages:

function example(name::String, format::Symbol)
    if format == :raw

    elseif format == :html

    end
end

In Julia, however, this system is just begging to be implemented into the language’s programming paradigm. Long blocks of conditionals to handle different operations is a thing of the past, as Julia allows us to easily replace almost any conditional statement with multiple dispatch using either a type or a type parameter.

abstract type AbstractFormat end

struct RawFormat <: AbstractFormat

end

struct HTMLFormat <: AbstractFormat

end

function save_to_format(save_uri::AbstractString, data::Any, format::AbstractFormat)
    open(save_uri, "w") do o::IOStream
        write(o, data_to_format(data, format))
    end
end

function data_to_format(data::Any, format::RawFormat)

end

function data_to_format(data::Any, format::HTMLFormat)

end

The example above works a lot better than before while also being a lot more readable. If our formats all shared the same data, or lack of data, we could easily further simplify this to a single parametric type:

struct Format{T <: Any}
end

function data_to_format(data::Any, format::Format{:html})

end

№8: The Pair and the pairs function

One incredibly underrated and extremely handy type from Julia is the Pair type. Although this data structure is often associated with the Dict type, the Pair is not exclusive to this application and is used all over Base Julia. One notable example would be the replace function:

replace("Hal is the coolest person in the world", "Hal" => "EMMA")

"EMMA is the coolest person in the world"

In my opinion, this is a surprisingly useful data structure. When I use other languages, it is surprising how often a good use-case for the Pair comes up, and it can be shocking how much this feature simplifies things while not getting much attention. Another huge thing that the Pair structure deserves credit for is simplifying the world of dictionaries in Julia:

mydct = Dict(e => e * 5 for e in 56:155)

Dict{Int64, Int64} with 100 entries:
  56  => 280
  114 => 570
  123 => 615
  110 => 550
  60  => 300
  136 => 680
  67  => 335
....

Because pairs are a convenient data structure we can provide to an array of functions across all of Julia, it makes a lot of sense to facilitate this data-type in our own work — someone can construct a dictionary, loop, or whatever else they need super easily. We can use Base.pairs to get the pairs back from our dictionary, and this function is also often bound for other types such as the DataFrame :

pairs(mydct)

Dict{Int64, Int64} with 100 entries:
  56  => 280
  114 => 570
.....

The Pair data structure is surprisingly subtle in its abilities, but after actually using the structure and seeing how many great places it can be used to solve classic problems, I think few developers could deny how useful this type is.

№9: Preallocating Output Arrays with similar()

Creating empty vectors and dynamically growing them inside loops using push! triggers continuous array resizes and memory reallocation. Preallocating arrays beforehand avoids this bottleneck entirely. To easily preallocate outputs, use similar(). It allocates an uninitialized array of the exact same element type, shape, and structure as an input array:

function transform_data(x::AbstractVector{T}) where T
    # Allocate empty uninitialized output of the exact same size & element type
    out = similar(x)

    for i in eachindex(x, out)
        out[i] = x[i]^2 + 2x[i] + 1
    end
    return out
end

x = [1.0, 2.5, 3.8]
transform_data(x)

№10: Default Argument Evaluation

Another concept I really wanted to discuss is Default Argument Evaluation. This is another one of those features in Julia that is easy to overlook, but is missed when you move from Julia to other languages. The ability to provide a default that evaluates at runtime is not only incredibly slick from a programming language perspective, it also does a lot to simplify code and adds convenience when creating a lot of functions.

Default Argument Evaluation allows us to calculate an optional positional argument or keyword argument when the function is called by getting from the provided arguments.

function process_data(data::Vector{Float64}, scale::Float64 = maximum(data))
    return data ./ scale
end

nums = [1.0, 5.0, 10.0]

process_data(nums)
process_data(nums, 2.0)

I know this might not seem like a life-changing aspect of Julia, and it isn’t going to completely revolutionize anyone’s code, but if we consider an alternative we might try in another language (Python) we see why I like this feature so much:

def process_data(data: list[float], scale: float | None = None) -> list[float]:
    if scale is None:
        scale = max(data)  # Forced into the function body
    return [x / scale for x in data]

# Calling code looks the same, but the implementation is much noisier
nums = [1.0, 5.0, 10.0]

process_data(nums) 
process_data(nums, 2.0)

Now imagine doing this for several keyword arguments and positional arguments within the function, and we see why this is far more tedious and laborious than it would be in Julia. Though it might seem like a specific use-case, in my opinion it is surprising how often this comes up. After using Julia for many years, this is another one of those features I often assume is in other languages before remembering I have to use the alternative.

№11: (Bonus) display and show

In Julia, **show and `display** are the two core functions responsible for converting objects into human-readable output, but they operate at different levels of abstraction and target different destinations. Theshowfunction is the fundamental function to write a visual representation of an object to an output stream. Thedisplayfunction is for presenting objects within interactive environments. Whereasshowis designed explicitly to work on types of IO,displayis more versatile and typically operates directly on a display system. When we build a new datatype, bindingshow` makes that structure a lot easier to work with.

struct MetricResult
    name::String
    score::Float64
end
# "Catch all"
Base.show(io::IO, m::MetricResult) = print(io, "$(m.name): $(m.score)")

# Show in plain text catch all
function Base.show(io::IO, ::MIME"text/plain", m::MetricResult)
    println(io, "📊 Metric Result:")
    println(io, "  ├─ Name  : ", m.name)
    println(io, "  └─ Score : ", round(m.score, digits=4))
end

# Rich HTML rendering for Jupyter/VS Code Notebooks
function Base.show(io::IO, ::MIME"text/html", m::MetricResult)
    print(io, "<div style='border:1px solid #ccc; padding:8px; border-radius:4px;'>",
              "<b>📊 $(m.name)</b>: <code>$(m.score)</code></div>")
end

The display function will automatically use the appropriate show function for the MIME we are displaying the type with. In the case of Jupyter, calling display will try to show our object with the text/html MIME first, so we will get the following in return:

If you’d like some more in-depth examples of using show on different projects, here is a link to one of my classic pieces with information on using this function:

[embed]Show And Display In Julia: Full Overview Showing different types in different ways with dispatch in julia!medium.com

№12: (Bonus) Distributed

Distributed is easily one of the coolest standard-library packages that the Julia programming language has to offer. The Distributed module allows us to easily distribute our tasks across multiple threads, instances of Julia, and even multiple machines. What makes Distributed so compelling isn't just its power, but its simplicity. Rather than forcing you to rewrite your code-base around complex message-passing protocols or external orchestration frameworks, Julia provides message-passing and process management as first-class citizens. With macros like @distributed and @everywhere, you can turn a bottlenecked sequential loop into a parallel workload running across a local cluster in just a couple of lines.

using Distributed

addprocs(4)

@everywhere using Random

total = @distributed (+) for i in 1:1_000_000
    rand()
end

println("Parallel sum across $(nprocs()) processes: ", total)

These @everywhere and @distributed macros can be used with local and non-local threads, making it exceedingly simple to distribute tasks across whatever machines are available to run them. The one major shortcoming to this package is that moving data across these threads can be a really difficult problem to solve at times. For those interested in learning this package in more detail, I wrote an overview two years ago that does a great job of explaining the basics:

[embed]Distributed.jl — Distribute Parallel Tasks In Julia With Ease A quick overview and tutorial on the Distributed package — parallel computing for julia.medium.com

If making things easier is more of your speed, I also have a process management package called [ParametricProcesses](https://chifidocs.com/parametric/ParametricProcesses) . Although this package has yet to implement certain things, like remote workers, it definitely simplifies the process of creating workers and loading data into them.

in closing

The major selling point for the Julia language is often its speed. While the language is rightly revered for its speed, I think an exclusive focus on the language’s runtime performance undersells a lot of the greatest qualities Julia has to offer. The most compelling reason to use Julia might be its speed, but in a world where Rust and Nim exist, Julia is not alone in being more accessible while being fast. But Julia still has things those languages don’t have; multiple dispatch as a programming paradigm, macros, parameterized types, default argument evaluation, and, most crucially, the ability to import and extend any function with new methods.

One of the coolest things about this list is that every point mentioned applies to the same programming language; it is incredible that a language has this much depth before we even consider using Pkg to add more packages. Something I really appreciate about this language is that it tries to re-engineer modern programming languages to do more than they’ve done before. Even if the language ceased to exist tomorrow, it has already been successful in introducing its ideas and conveniences to the world of programming… and I think that is definitely worth something. In conclusion, Julia is awesome and totally worth picking up. Thank you for reading.


메타데이터
post_id
3ecfcd77b77a
slug
10-random-julia-tips-to-help-you-on-your-way-3ecfcd77b77a
url
https://medium.com/chifi-media/10-random-julia-tips-to-help-you-on-your-way-3ecfcd77b77a
canonical_url
https://medium.com/chifi-media/10-random-julia-tips-to-help-you-on-your-way-3ecfcd77b77a
author_url
https://medium.com/@emmaccode
status
ok
fetched_at
2026-08-10 16:44:30