User Tools

Site Tools


controlflow
snippet.juliarepl
julia> pkgchk( [ "julia" => v"1.0.2" ] )

Control Flow Notes

This chapter assumes familiarity with other programming languages. It describes some aspects of control flow statements that are not in most other Algol-derived languages. It covers conditionals, Booleans, exceptions, channels, etc.

In Julia 1.0.1, the use of global variable inside functions can dramatically slow down julia. This is not the case if the variables are declared const. (Local variables cannot be declared const.) Advice:

  1. avoid the use of global variables inside functions.
  2. if the use of globals inside functions is unavoidable, see if a const version of the global variable can do.
  3. if you need a modifiable global variable, be aware that this may badly impact performance all around.

Comments

snippet.julianoeval
[download only julia statements]
julia>		## this comment goes to the line end.  no multi-line comments in 1.0 afaik
 
julia> """
 
but you can define
an informative
string and then
simply ignore it
later on.
 
""";

Spaces

Spaces are often but not always ignored. For example,

snippet.juliarepl
julia> ( [ 12 ] == [ 12] )	## the first is a 1x2 matrix; the second is a 1x1 vector
false
  • Julia sometimes requires spacing–e.g., in the trinary (true) ? 1 : 0.
  • Julia also insists on no extra space between function name and opening parenthesis.

PS: Spaces have always had meaning in tokens, such as && or == in other languages, too.

Grouping Multiple Statements

snippet.juliarepl
julia> (x = 3; y = 4)		## the last answer is always stored into 'ans' (and usually printed)
4

julia> begin; x= 4; y= 5; end;	## suppress printing ans with trailing semicolon

julia> ans			## ans was still assigned, of course
5

The version with begin and end behaves the same as the version with ( and ).

Parentheses for Grouping Vs. Tuples

Parentheses serve other functions, too–specficially also for read-only lists of objects called tuples. This can cause non-descript or baffling errors:

snippet.juliarepl
julia> (const x= 3; const y= 4)		## grouped content, two statements
4

julia> typeof( ans )
Int 64

julia> (x= 3, y= 4)			## the comma instead of semi-colon
(x = 3, y = 4)

julia> typeof( ans )			## ... means that you have defined a named tuple
NamedTuple{(:x, :y),Tuple{Int64,Int64}}

julia> (  (  ( (true) )  )  )		## all parentheses here do grouping, *not* tupling
true

julia> (true,)				## this parenthesis is a single tuple, indicated by the trailing (otherwise useless) comma
(true,)

julia> (  (  ( (true,false) )  )  )	## not nested singleton-tuplets! only innermost defines tuple, rest are groupings
(true, false)

julia> (  (  ( (true), )  ),  )		## two groupings, two tuplings
((true,),)

Line Continuation

There is no explicit line continuation symbol (like '\' at the line end). Instead, use

snippet.juliarepl
julia> (const x= 3;
         const y= 4);			## grouping automatically extends over multiple lines ##

Multiple Assignments

snippet.juliarepl
julia> const a, b= 10, 12		## two assignments (to const a and const b)
(10,12)

julia> b
12

julia> const (c, d)= (2, 3)		## same assignment
(2,3)

julia> c
2

Inferred Multiplication

snippet.juliarepl
julia> 2pi			## no ship.  this is 2*pi
6.283185307179586

julia> const x=3; 2^2x		## binds closer, so this is not 4*3, but 2^6
64

Symbols and Variable Names (Colon, Eval)

Symbols are a specific type. They are primarily useful when they represent “clean” strings, meaning that they could be variable names–no spaces, not starting with digits, etc.

snippet.juliarepl
julia> const x= 3
3

julia> :x			## typeof(:x) is Symbol
:x

julia> eval(:x)
3

julia> dump(:x)			## dump is the universal rat (informant)
Symbol x

julia> Symbol("x")
:x

However, symbols can also hold expressions. :a+3 is an error, because it means (:a)+3; but :(a+3) is ok.

snippet.juliarepl
julia> Symbol("   2 + 1")		## useless symbol.  note the ans without ':'
Symbol("   2 + 1")

julia> eval(ans)
ERROR: UndefVarError:    2 + 1 not defined
Stacktrace:

To convert to string "x", use String( :x ). To convert from string to :x, use Symbol("x"). Usefully, you can also write eval(:x) to get the content of the variable x.

Function Chaining

snippet.juliarepl
julia> exp( log(5) )		## Nested function
5.0

julia> (exp∘log)(5)		## Same nesting, but written differently
5.0

julia> log(5) |> exp		## Same nesting, but written differently
5.0
  • This syntactic sugar works only for functions with only one argument. If a function has other arguments, you could f(g(garg),farg) == g(garg) |> x->f(x,farg).
  • With lazy evaluation lists, you can do @> x g f(y, z) == f(g(x), y, z).

Function Semicolons

snippet.juliarepl
julia> function f(); 1; end#function;		## works as expected

julia> function f() 1 end#function;		## also works as expected

Booleans and Conditionals

snippet.juliarepl
julia> ((true) || (false)) && "proper short-circuiting"		## && is "and"; || is "or";  use can be "perl"-like
"proper short-circuiting"

julia> (1) && print("ints do not work as booleans; use (x!= 0)")	## picky: an integer is not Boolean
ERROR: TypeError: non-boolean (Int64) used in boolean context
Stacktrace:

However, unlike in perl, && has precedence over =, which causes an error in the following construct:

snippet.juliarepl
julia> (true) && x= 3		## means ((true) && x)= 3
ERROR: syntax: invalid assignment location "true && x"

julia> (true) && (x= 3)		## this is probably what you really wanted
3
  • julia has elseif.
  • It is possible to use the `isdefined()`] macro defined in [[inquiring#checking_if_an_object_exists|inquiring to establish default values (as in perl's $a= $ARGV[0] || 10). However, this is rarely useful in Julia, because variables are typed and compiled, and thus cannot be truly “empty” (as they can in perl or R).
  • For example, for command line arguments, ARGS is always defined. It starts as an x=0 x-element Array{String,1}, and the Julia test for whether there are arguments would be a= (length(ARGS)>0) || 10.

Trinary Conditionals

snippet.juliarepl
julia> (true) ? "10" : "20"				## works only for scalars
"10"

julia> (true)?"10":"20"					## watch for spaces
ERROR: syntax: space required before "?" operator

julia> ifelse.( [true,false], [1,1], [2,2] )		## for arrays, use ifelse.
2-element Array{Int64,1}:
 1
 2

Multiple Conditions (Chained Comparisons)

Julia has unusual chained comparisons,

snippet.juliarepl
julia> 1 <= 2 <= 3	## same as ((1 <= 2) && (2 <= 3))
true

julia> 1 .<= (0, 2, 4, 6) .<= 4
(false, true, true, false)

(The dot-prefix in the .<= operator denotes the element-wise comparison.)

Loops

Ordinary Program Logic

  • while works as expected.
  • Loops can use break and continue.

For Loops

There is no generic C-style three-part for(start; cond; iter). Only the following two for constructs are available:

snippet.juliarepl
julia> for i in ("a", "b") ; end;

julia> for i= 1:2 ; end;

More unusual, nested for loops can be represented in one statement:

snippet.juliarepl
julia> for i= 1:2, j= 3:4
           println( (i, j) )
       end#for^2##
(1, 3)
(1, 4)
(2, 3)
(2, 4)

Special Loops for Vectors and Tuples

Comprehensions (Generation of Vectors)

snippet.juliarepl
julia> y= (x for x= 1:3)
Base.Generator{UnitRange{Int64},getfield(Main, Symbol("##7#8"))}(getfield(Main, Symbol("##7#8"))(), 1:3)

julia> collect(y)		## convert the iterator into a simpler array
3-element Array{Int64,1}:
 1
 2
 3

Maps (Transformation)

snippet.juliarepl
julia> map( x->(x+5), (1,2,3) )			## apply function in first arg to each element of second arg
(6, 7, 8)

julia> map( (1,2,3) ) do x; x+5; end#do		## internally, `do` creates an anon function as arg1
(6, 7, 8)

Filters (Removal)

snippet.juliarepl
julia> filter( x->(x >= 2), [1,2,3] )
2-element Array{Int64,1}:
 2
 3

Scope: Global, Local, Nested

WARNING Top-level global variables are different from variables one or a few levels up. This does not always make intuitive sense. Moreover, it is possible that future versions of julia may change the semantics somewhat. (For discussion, please see discourse and github and discourse.)

Newly defined variables inside a scope are local—they disappear upon leaving scope. Local scopes are introduced with

  • flow control: for, while, let, struct, do...end
  • macros, and
  • functions

but surprisingly not by begin...end.

A new variable instantiated inside a local scope can also survive the local scope if it is preceded with global. However, it is usually better and clearer to define such variables in front of the the scope to begin with.

snippet.juliarepl
julia> global x=0	## makes it clear that we will want this variable
0

julia> for i=1:2
	global x	## needed!  this is even though we are not in a function.
        x+= 1
       end#for##

julia> x
2
  • The global/local behavior does not always seem consistent. In a for loop, the inside variable is by default local. In an if statement, an inside variable is by default global. Just be careful.
  • To define a new variable with a name equal to one in an outer scope, use local. However, it is usually better and clearer to choose a different name.

A Function Modifying a Global Object

snippet.juliarepl
julia> x, y= –1,2;						## global variables

julia> function inner1(); x= 10; x+y; end;##function##		## x is a local variable

julia> inner1()
 8

julia>  x1

julia> function inner2(); global x= 10; x+y; end;##function##	## x is the global variable

julia> inner2()
 8

julia>  x
 10

A Function Modifying A Variable In An Upper Scope

Nested functions can change up-scope values (global is not an up-scope):

snippet.juliarepl
julia> function outer()
	    x, y= –1,2
            function inner(); x= 10; x+y; end#function inner
	    ( inner() , x )
	end;#function outer##

julia> outer()
(8, 10)
  • PS: This “nested trick” is actually what facilitates static state variables inside functions with let. Let can “fix” the current value of a variable at function definition time, rather than use-time, too. The fancy compsci term for this behavior is closure. See functions.

Warnings and Errors

To suppress other packages' stdout or stderr output, you can use Suppressor Of course, this can be quite dangerous–your code may not work, and without any error appearing, you may end up thoroughly confused.

Julia's logging system is discussed in its documentation.

stderr

To print to stderr, use println(stderr, ...).

@info

@info() emits cyan highlighted text to stderr, preceded by [ info.

@warn

@warn emits yellow highlighted text to stderr, preceded by standardized warning message.

@error

@error emits a red highlighted text to stderr, preceeded by a standardized error message.

Throwing an error

The macro @error only logs an error message. It does not throw an exception. To throw an exception, use the error() function.

@assert

The macro @assert that allows you to provide a useful error message:

snippet.juliarepl
julia> const x= (99);

julia> (x>0) || @assert false "your variable x is $x (but x^2= $(x^2) would work!)"
ERROR: AssertionError: your variable x is –99 (but x^2= 9801 would work!)
Stacktrace:

julia> @assert( true, "everything is a-ok" )	## much better!

Exceptions

Exception (Error) Trapping

Try/catch/finally works as expected:

snippet.juliarepl
julia> f(x)= try
           sqrt(x)
       catch optionalerrormsgname
           println(optionalerrormsgname)
           sqrt( complex(x, 0) )
       end;##try##

julia> f(1)
DomainError(1.0, "sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).")
0.0 + 1.0im

A finally clause can guarantee execution at the end.

Disabling Exceptions For One Function

To guarantee that a function that may throw an error (e.g., DomainError) will not do so, you can define

snippet.juliarepl
julia> function err2nan( fin::Function )::Function
    wrapped(x)= ( y= NaN;  try y= fin(x); catch; NaN; end; y )
    wrapped
end;##function##

julia> safesqrt= err2nan( sqrt );

julia> safesqrt(1.0 )
NaN
  • safesqrt works, but it is very inefficient compared to sqrt. For this particularly case, you could use NaNMath for better performance.

Throwing Non-Specialized Errors

snippet.juliarepl
julia> fussy_sqrt(x)= (x >= 0) ? sqrt(x) : error("negative x of $x not allowed")
fussy_sqrt (generic function with 1 method)

julia> fussy_sqrt(1)
ERROR: negative x of –1 not allowed
Stacktrace:
  • Many programmers writing primarily for themselves care less about the hierarchy or naming of exception types within subsets. In these cases, the generic error() exception works well. For more specialized exceptions, see below.

Throwing Specialized Exceptions

snippet.juliarepl
julia> f(x)= (x >= 0) ? exp(-x) : throw(DomainError())
f (generic function with 1 method)

julia> throw( UndefVarError(:x) )
ERROR: UndefVarError: x not defined
Stacktrace:

The generic error() throws an ErrorException.

Stop()ping Execution of Program

snippet.juliarepl
julia> struct Stop <: Exception end

julia> Base.display_error(io::IO, ::Stop, _)= println( io, "STOP STATEMENT ENCOUNTERED AT ", @__FILE__, ":", @__LINE__ )

julia> stop(msg::AbstractString= "")= println(msg), throw(Stop())
stop (generic function with 2 methods)

julia> function g(); stop("in G, sort of not an error but a stop"); println("gend"); end; function f(); g(); println("fend"); end
f (generic function with 1 method)

julia> f()
in G, sort of not an error but a stop
STOP STATEMENT ENCOUNTERED AT none:1
  • See also ?showerror.

Trapping Operating System Signals, ^C

The following code works on *nix, but not on Windows:

snippet.julianoeval
[download only julia statements]
julia> try
           # code that might take a while, e.g.
           println("Please type ^C")
           sleep(1000)
       catch ex
           if ex isa InterruptException
               println("ok, you interrupted me")
           else
               rethrow()
           end
       end
^Cok, you interrupted me.

Setting a Program Timeout

snippet.julirepl
[download only julia statements]
julia> function timeout(timelimit, func)
    t1 = Task(func)
    t2 = Task() do
            sleep(timelimit)
            schedule(t1, "execution of closure didn't finish within the $timelimit seconds time limit", error=true)
    end
    schedule(t1)
    schedule(t2)
    s = fetch(t1)
    return s
end
 
julia> timeout(2, () -> sleep(5))
ERROR: "execution of closure didn't finish within the 2 seconds time limit"
Stacktrace:
 [1] try_yieldto(::typeof(Base.ensure_rescheduled), ::Base.RefValue{Task}) at ./event.jl:196
 [2] wait() at ./event.jl:255
 [3] wait(::Condition) at ./event.jl:46
 [4] wait(::Task) at ./task.jl:188
 [5] fetch at ./task.jl:202 [inlined]
 [6] timeoutf(::Int64, ::Function) at ./untitled-0e9329369e7380cb5b6844213297ccf6:24
 [7] top-level scope at none:0
  • This function is especially useful with @async, so it may be better located in parallel.

Poor Man's Unit Test Code (at File's End)

Julia has complete testing facilities built in.

The poor man's way of unit-testing: each function sits in its own file and is accompanied at its end by a set of tests.

snippet.julianoeval
[download only julia statements]
if ( (PROGRAM_FILE != "") && (!endswith(@__FILE__, PROGRAM_FILE)) )
	@assert( f(0.2) == 1, "f(0.2) does not yield 1, as I thought it would, but $(f(0.2))" )
	...
end#if
  • You can run the tests interactively by setting PROGRAM_FILE to the current filename.
  • This could become something like test(fnm)= ( op= PROGRAM_FILE; PROGRAM_FILE= fnm; try; include(fnm); finally: PROGRAM_FILE= op; end; ) and used as test("filename.jl").

Tasks and Channels

Tasks and channels do not exist in many other languages (like C or perl).

Channels

Channels are functions that act as FIFO queues. Thus, they can be used to make a “factory.” Channels can obviate the need for state variables in the factory function itself.

snippet.juliarepl
julia> function producer(c::Channel)
            put!(c, "start")						## caller will first stop here and return
            for n= 1:2
                put!(c, 2n)						## then caller will stop here and return
            end#for
            put!(c, "no more")						## finally caller will stop here and return
        end;#function##

julia> cnsmchnl= Channel(producer);					## the producer function will feed, Main will consume

julia> take!(cnsmchnl)
"start"

julia> take!(cnsmchnl)
2

julia> take!(cnsmchnl)
4

julia> take!(cnsmchnl)							## closes, because producer() will reach end of function
"no more"

julia> take!(cnsmchnl)
ERROR: InvalidStateException("Channel is closed.", :closed)
Stacktrace:
  • Channel functioning is nicely obvious in single-core operation.
  • Channels can often serve as C-like “static” local variables.
  • Channels do not yet support multi-core operation.
  • Channels can be closed at any time, even if they have not yet been exhausted.
  • It is possible to pass arguments to a channel with take! using a convenience macro
  • It is possible with yieldto() for a producer and consumer to collaborate. This can be further coordinated using current_task(), istaskdone(), istaskstarted(), task_local_storage().
  • Channels can also be buffered for better performance (filled with content waiting to be taken later). The above channel was unbuffered. It just created the return items when requested.

Backmatter

Useful Packages on Julia Repository

Notes

  • Discuss Interfaces for
    • Iteration (start, next, done; and more)
    • Indexing (getindex, setindex, endof) and AbstractArray (advancing index, etc.)

References

controlflow.txt · Last modified: 2018/12/05 19:52 (external edit)