|
| 1 | ++++ |
| 2 | +cover = false |
| 3 | ++++ |
| 4 | + |
| 5 | +# Tips and Tricks for Julia programming |
| 6 | + |
| 7 | +At this point there is a wealth of information for the Julia programming |
| 8 | +language. Perhaps one of the best sources (outside of the official documentation |
| 9 | +or discourse forum) is [JuliaNotes.jl](https://m3g.github.io/JuliaNotes.jl/stable/). |
| 10 | +Personally, I find that more available information is better and since I'm a |
| 11 | +big fan of Julia, I've decided to make my own contribution. |
| 12 | + |
| 13 | +Here I'll outline some basic, but at times lesser-known, tips and tricks for |
| 14 | +writing Julia code. |
| 15 | + |
| 16 | +## Ternary Conditional Operator |
| 17 | + |
| 18 | +Julia has the ternary operator for `if-else` statements. The best way to explain |
| 19 | +it is to show two equivalent blocks of code. First, one that uses `if-else` |
| 20 | + |
| 21 | +```julia |
| 22 | +for i = 1:10 |
| 23 | + if i > 5 |
| 24 | + println("big") |
| 25 | + else |
| 26 | + println("small") |
| 27 | + end |
| 28 | +end |
| 29 | +``` |
| 30 | + |
| 31 | +This lengthy `if-else` statement can be replaced with a ternary operator while |
| 32 | +still preserving the logic in the code. |
| 33 | + |
| 34 | +```julia |
| 35 | +for i = 1:10 |
| 36 | + i > 5 ? println("big") : println("small") |
| 37 | +end |
| 38 | +``` |
| 39 | + |
| 40 | +Within this ternary expression, the `?` operator denotes which boolean expression |
| 41 | +(what comes before it) is being evaluated. The `:` operator separates the `if` |
| 42 | +(before the `:`) and `else` (after the `:`) outcomes. This can help shrink |
| 43 | +lengthy code blocks, but it comes with the additional cost of increased code |
| 44 | +complexity. However, once familiar with the ternary operator, the complexity |
| 45 | +of the expression goes away. |
| 46 | + |
| 47 | +## Short Circuit Expressions |
| 48 | + |
| 49 | +In Julia the short circuit operators are `&&` (`and` operator), and `||` |
| 50 | +(`or` operator). The most basic use for them is conditional statements. |
| 51 | + |
| 52 | +```julia |
| 53 | +x = 5 |
| 54 | + |
| 55 | +if (x > 3) && (x < 6) |
| 56 | + println("True") |
| 57 | +end |
| 58 | +``` |
| 59 | + |
| 60 | +In strictly-typed languages, these expressions can only evaluate to a boolean, |
| 61 | +as is the case in the example above. However, Julia is a loosely-typed language, |
| 62 | +which affords it the ability to return the last value in this expression. This |
| 63 | +becomes a powerful tool for simplifying conditional statements, although some |
| 64 | +argue that it unnecessarily increases the complexity of the code. Below I show |
| 65 | +how you can convert the `if` statement above into a one-line short circuit expression. |
| 66 | + |
| 67 | +```julia |
| 68 | +x = 5 |
| 69 | + |
| 70 | +(x > 3) && (x < 6) && println("True") |
| 71 | +``` |
| 72 | + |
| 73 | +This expression is equivalent to the `if` statement; as such, it only prints |
| 74 | +`True` if both conditionals are satisfied (in this case they are). Since both |
| 75 | +code blocks shown are equivalent, the choice for which to use comes down to |
| 76 | +personal preference (or the repo style guide). I personally find it convenient |
| 77 | +to use these statements for `continue` conditionals within loops. |
| 78 | + |
| 79 | +```julia |
| 80 | +for i = 1:10 |
| 81 | + i == 5 && continue |
| 82 | + println(i) |
| 83 | +end |
| 84 | +``` |
| 85 | +## Pipes |
| 86 | + |
| 87 | +If you are familiar with Bash scripting in Linux, then you will already be |
| 88 | +familiar with pipes. In Julia, the pipe operator is `|>`. They can be used to |
| 89 | +redirect the output of something into another function. For example, if you |
| 90 | +have functions `foo` and `bar` such that you can run `bar(foo(x))`, then you |
| 91 | +can also rewrite this as `foo(x) |> bar`. A handy use case for pipes is to |
| 92 | +avoid defining multiple temporary variables, while also avoiding hard-to-read |
| 93 | +nested calls. |
| 94 | + |
| 95 | +```julia |
| 96 | +foo(x) = 2 * sin(x) |
| 97 | +bar(x) = 3 * x^2 |
| 98 | + |
| 99 | +# Several tmp vars |
| 100 | +tmp1 = rand() |
| 101 | +tmp2 = foo(tmp1) |
| 102 | +x = bar(tmp2) |
| 103 | + |
| 104 | +# Multiple nested calls |
| 105 | +x = bar(foo(rand())) |
| 106 | + |
| 107 | +# One clean easy to read line |
| 108 | +x = rand() |> foo |> bar |
| 109 | +``` |
| 110 | + |
| 111 | +## Anonymous Functions |
| 112 | + |
| 113 | +An anonymous function is (as the name suggests) a function without a name. |
| 114 | +In Julia, these are done by using the `->` operator. A simple `f(x) = 2 * x^2` |
| 115 | +function can be anonymized as `x -> 2 * x^2`; however, this example does not |
| 116 | +properly showcase the value of these anonymous functions. Although they can be |
| 117 | +used in numerous ways, they are most often used in two ways. |
| 118 | + |
| 119 | +1) Alongside pipes |
| 120 | + |
| 121 | +Sometimes you want to pipe output into a function that takes multiple arguments. |
| 122 | +In those cases, you need to couple a pipe and an anonymous function. The example |
| 123 | +below shows a simple version of this use case. |
| 124 | + |
| 125 | +```julia |
| 126 | +foo(x) = 6 * x^2 |
| 127 | +bar(x,y) = x^2 - y^2 |
| 128 | + |
| 129 | +z = rand() |> foo |> (x -> bar(x, 6)) |
| 130 | +``` |
| 131 | + |
| 132 | +In this example the output of `foo` is piped into an anonymous function |
| 133 | +(`x -> bar(x, 6)`) which passes the output into `bar` alongside an additional |
| 134 | +argument. |
| 135 | + |
| 136 | +2) Within function calls |
| 137 | + |
| 138 | +Many built-in functions take an expression/function as an argument, for instance, |
| 139 | +`filter` and `find` take expressions that evaluate to booleans as arguments. |
| 140 | + |
| 141 | +```julia |
| 142 | +x = rand(50) |
| 143 | + |
| 144 | +filter(e -> e > 0.5, x) |
| 145 | +``` |
| 146 | + |
| 147 | +The code block above uses the anonymous function `e -> e > 0.5` as the |
| 148 | +expression argument for `filter`, which in this case filters out the values |
| 149 | +larger than 0.5 from the array `x`. |
| 150 | + |
| 151 | +## Base Functions with Custom Types |
| 152 | + |
| 153 | +This feature involves Julia's multiple dispatch, which is simply when multiple |
| 154 | +functions have the same name but different arguments. This allows you to |
| 155 | +write custom base functions for custom types. This is easier to understand |
| 156 | +through an example. |
| 157 | + |
| 158 | +```Julia |
| 159 | +struct Point{F <: Float64} |
| 160 | + x::F |
| 161 | + y::F |
| 162 | + z::F |
| 163 | +end |
| 164 | + |
| 165 | +function Base.show(io::IO, point::Point) |
| 166 | + println(io, "(x,y,z) = ($(point.x),$(point.y),$(point.z))") |
| 167 | +end |
| 168 | +``` |
| 169 | + |
| 170 | +The example above shows how to make a custom `show` function for a custom |
| 171 | +type `Point`. This is called "extending" the `show` function. This makes it |
| 172 | +so that when you display variables of the type `Point`, they will be displayed |
| 173 | +using the custom `show` function above. This means that when instantiating a |
| 174 | +variable `p = Point(1.0, 2.0, 3.0)` in the `repl`, the following output will |
| 175 | +be `(x,y,z) = (1.0,2.0,3.0)`. |
| 176 | + |
| 177 | +This can be done to all functions you have access to within your Julia code, |
| 178 | +even those from other imported packages. An important consideration here is |
| 179 | +to make sure you avoid "type piracy", where you extend a function on types |
| 180 | +that you did not define. For example, |
| 181 | + |
| 182 | +```julia |
| 183 | +Base.show(io::IO, f::Float64) = println(io, "This is type piracy") |
| 184 | +``` |
| 185 | + |
| 186 | +extending `show` on the `Float64` type (which would not be extending but |
| 187 | +rather "redefining", since it already exists) is a case of type piracy. |
| 188 | +This is considered bad practice since it can cause unexpected behavior within |
| 189 | +your code, or someone else's code if they import your code. |
0 commit comments