3.5 Decision Making

In everyday life people have to make decisions and so do computer programs. This is the job for if ... elseif ... else constructs.

3.5.1 If …, or Else …

To demonstrate decision making in action let’s say I want to write a function that accepts an integer as an argument and returns its textual representation. Here we go.

function turnInt2string(num::Int)::String
    if num == 0
        return "zero"
    elseif num == 1
        return "one"
    elseif num == 2
        return "two"
    else
        return "three or above"
    end
end

(turnInt2string(2), turnInt2string(5)) # a tuple with results
("two", "three or above")

The general structure of the construct goes like this:

# pseudocode, don't run this snippet
if (condition_that_returns_Bool)
    what_to_do
elseif (another_condition_that_returns_Bool)
    what_to_do
elseif (another_condition_that_returns_Bool)
    what_to_do
else
    what_to_do
end

As mentioned in Section 3.3.4 Bool type can take one of two values true or false. The code inside if/elseif clause runs only when the condition is true. You can have any number of elseif clauses. Only the code for the first true clause runs. If none of the previous conditions matches (each and every one is false) the code in the else block is executed. Only if and end keywords are obligatory, the rest is not, so you may use

# pseudocode, don't run this snippet
if (condition_that_returns_Bool)
    what_to_do
end

or

# pseudocode, don't run this snippet
if (condition_that_returns_Bool)
    what_to_do
else
    what_to_do
end

or

# pseudocode, don't run this snippet
if (condition_that_returns_Bool)
    what_to_do
elseif (condition_that_returns_Bool)
    what_to_do
else
    what_to_do
end

or

# pseudocode, don't run this snippet
if (condition_that_returns_Bool)
    what_to_do
elseif (condition_that_returns_Bool)
    what_to_do
elseif (condition_that_returns_Bool)
    what_to_do
else
    what_to_do
end

or …, never mind, I think you got the point.

Below I place another example of a function using if/elseif/else construct (in order to remember it better).

# works fine for non-empty vectors
function getMin(vect::Vector{Int}, isSortedAsc::Bool)::Int
    if isSortedAsc
        return vect[1]
    else
        sortedVect::Vector{Int} = sort(vect)
        return sortedVect[1]
    end
end

x = [1, 2, 3, 4]
y = [3, 4, 1, 2]

(getMin(x, true), getMin(y, false))
(1, 1)

Here I wrote a function that finds the minimal value in a vector of integers. If the vector is sorted in the ascending order it returns the first element. If it is not, it sorts the vector using the built in sort function and returns its first element (this may not be the most efficient method but it works). Note that the else block contains two lines of code (it could contain more if necessary, and so could if block). I did this for demonstrative purposes. Alternatively instead those two lines (in the else block) one could write return sort(vect)[1] and it would work just fine.

3.5.2 Ternary expression

If you need only a single if ... else in your code, then you may prefer to replace it with ternary operator. Its general form is condition_or_Bool ? result_if_true : result_if_false.

Let me rewrite getMin from Section 3.5.1 using ternary expression.

function getMin(vect::Vector{Int}, isSortedAsc::Bool)::Int
    return isSortedAsc ? vect[1] : sort(vect)[1]
end

x = [1, 2, 3, 4]
y = [3, 4, 1, 2]

(getMin(x, true), getMin(y, false))
(1, 1)

Much less code, works the same. Still, I would not overuse it. For more than a single condition it is usually harder to write, read, and process in your head than the good old if/elseif/else block.

3.5.3 Dictionaries

Dictionaries in Julia are a sort of mapping. Just like an ordinary dictionary is a mapping between a word and its definition. Here, we say that the mapping is between key and value. For instance let’s say I want to define an English-Polish dictionary.

engPolDict::Dict{String, String} = Dict("one" => "jeden", "two" => "dwa")
engPolDict # the key order is not preserved on different computers
Dict{String, String} with 2 entries:
  "two" => "dwa"
  "one" => "jeden"

Here I defined a dictionary of type Dict{String, String}, so, both key and value are of textual type (String). The order of the keys is not preserved (this data structure cares more about lookup performance and not about the order of the keys). Therefore, you may see a different order of items after executing the code on your computer.

If we want to now how to say “two” in Polish I type aDict[key] (if the key is not there you will get an error), e.g.

engPolDict["two"]

dwa

To add a new value to a dictionary (or to update the existing value) write aDict[key] = newVal. Right now the key “three” does not exist in engPolDict so I would get an error (check it out), but if I type:

engPolDict["three"] = "trzy"

trzy

Then I create (or update if it was already there) a key-value mapping.

Now, to avoid getting errors due to non-existing keys I can use the built in get function. You use it in the form get(collection, key, default), e.g. right now the word “four” (key) is not in a dictionary so I should get an error (check it out). But wait, there is get.

get(engPolDict, "four", "not found")

not found

OK, what anything of it got to do with if/elseif/else and decision making. The thing is that if you got a lot of decisions to make then probably you will be better off with a dictionary. Compare

function translEng2polVer1(engWord::String)::String
    if engWord == "one"
        return "jeden"
    elseif engWord == "two"
        return "dwa"
    elseif engWord == "three"
        return "trzy"
    elseif engWord == "four"
        return "cztery"
    else
        return "not found"
    end
end

(translEng2polVer1("three"), translEng2polVer1("ten"))
("trzy", "not found")

with

function translEng2polVer2(engWord::String,
                           aDict::Dict{String, String} = engPolDict)::String
    return get(aDict, engWord, "not found")
end

(translEng2polVer2("three"), translEng2polVer2("twelve"))
("trzy", "not found")

Note: Dictionaries like Arrays (see Section 3.3.7) are passed by references

In translEng2polVer2 I used a so called optional argument for aDict (aDict::Dict{String, String} = engPolDict). This means that if the function is provided without the second argument then engPolDict will be used as its second argument. If I defined the function as translEng2polVer2(engWord::String, aDict::Dict{String, String}) then while running the function I would have to write (translEng2polVer2("three", engPolDict), translEng2polVer2("twelve", engPolDict)). Of course, I may prefer to use some other English-Polish dictionary (perhaps the one found on the internet) like so translEng2polVer2("three", betterEngPolDict) instead of using the default engPolDict we got here.

In general, the more if ... elseif ... else comparisons you got to do the better off you are when you use dictionaries (especially that they could be written by someone else, you just use them). Still, in the rest of the book we will probably use dictionaries for data storage and a quick lookup.

OK, enough of that. If you want to know more about conditional evaluation check this part of Julia’s docs.



CC BY-NC-SA 4.0 Bartlomiej Lukaszuk