User Tools

Site Tools


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

FIXME This chapter needs major reorganization

Julia Functions

Prior Coverage

  • Inquiring already described some function-related aspects of Julia. For example,
    1. Function naming conventions: postfix! alters the argument, postfix. applies a scalar function element-wise to an array, and @fun calls a macro at compile time.
    2. To learn about existing functions:

      * methods(sqrt) gives all versions of sqrt * methodswith(Bool) gives all functions operating on Booleans * @which sqrt( 5 ) tells which function will be called for argument '5'

  • A function (like sqrt()) can have multiple declarations, depending on input type. Each “function” version is called a “method.” For example, in the case of the function Base.sqrt, there are 10 methods.

Function Types (and Function Names)

Function names were already mentioned and used repeatedly (e.g., in julia_s_function_names). This is a more elaborate explanation.

AT-PREFIX FUNCTIONS (@):

These are function implemented as macros. They are similar to the preprocessor macro functions that are in C (e.g. #define). Their definitions have access to types, but not to variables.

snippet.juliarepl
julia> using Printf

julia> @printf "v = %0.2f" 0.2345
v = 0.23

julia> @assert(false,"do not know what's happening")
ERROR: AssertionError: do not know what's happening
Stacktrace:

julia> @boundscheck @assert(1>0, "code only works when 1 is positive")
  • Many of the most useful @ functions relate to such things as type-checking at compile-time, assertions, parallelization, and bounds-checking.
  • It is occasionally useful for ordinary end users to create macros. Emphasis on occasional. For the most part, they can be considered like LaTeX style files to authors—magic that someone else wrote, that works, and that can be but is better not tinkered with. If you do not know what I am talking about, don't worry.

DOT Functions and Operators

Programmers need not write dot-equivalent functions. Julia automatically understands that a scalar 'dot function' applied to an array should work on each element. This works via the Broadcasting feature.

snippet.juliarepl
julia> mysqrt(x) = sqrt(x)
mysqrt (generic function with 1 method)

julia> mysqrt.( [1,2,3] )
3-element Array{Float64,1}:
 1.0
 1.4142135623730951
 1.7320508075688772

DOT-PREFIX OPERATORS (.):

When prefixed with a '.', the operator works element-wise on iterables (tuples, arrays, iterators, etc). (It does not work on structs.

snippet.juliarepl
julia> [1,2,1] .>= [3,1,0]               ## compare element-wise the contents of vectors
3-element BitArray{1}:
 false
  true
  true

Not shown, [1,2,1] >= [3,1,0] gives an error, because '>=' is only defined for scalars and not for vectors.

FIXME Explain the Ref() to indicate scalar argument to vector function. When is it necessary? Why now and not before?

DOT-POSTFIX FUNCTIONS (.):

Dots denote vector functions, just like dot operators, but the dot postfixes functions, rather than prefixes them.

By default, any function defined on scalars works element-wise on the contents of arrays, too, given the dot-postfix. That is, the programmer need only define the scalar function, and julia automatically recognizes the dot-postfix function for vectors. See Dot Syntax for Vectorizing Functions.

snippet.text
[download only julia statements]
julia> my_fun( x ) = sqrt( x )              ## define a new scalar function
my_fun (generic function with 1 method)
 
julia> my_fun( [1, 2] )                     ## scalar function should not be used on vector
WARNING: sqrt(x::AbstractArray{T}) where T <: Number is deprecated, use sqrt.(x) instead.
 
julia> my_fun.( [1, 2] )                    ## with postfix dot, apply scalar op to each element of vector
2-element Array{Float64,1}:
 1.0
 1.41421

Eventually, the warnings will become errors. Get used to using dot-postfix notation for invoking function on elements of arrays.

There are rare reasons for a programmer to define a dot-postfix function herself, e.g., when the vector version of a function calls external vectorized GPU-code; or when functions have different meanings for scalars and arrays (e.g., multiplication).

EXCLAMATION-POSTFIX FUNCTIONS (!):

By convention, functions should be postfixed with '!' when they plan to change their passed objects in-place. Julia names these “bang” functions. That is, any exclamation-postfix named function should be expected to tinker with its operands:

snippet.juliarepl
julia> x= [1,2]; push!(x, 3)         ## expect x to be be modified
3-element Array{Int64,1}:
 1
 2
 3

julia> x
3-element Array{Int64,1}:
 1
 2
 3

julia> push!( [1,2], 3 )             ## allowed, but '!' is useless, because [1,2] is ephemeral
3-element Array{Int64,1}:
 1
 2
 3

Sensibly, exclamation-postfix functions can operate on read-write arrays, but not on read-only tuples:

snippet.juliarepl
julia> push!( (1,2) , 3 )
ERROR: MethodError: no method matching push!(::Tuple{Int64,Int64}, ::Int64)
Closest candidates are:
  push!(::Any, ::Any, !Matched::Any) at abstractarray.jl:2064
  push!(::Any, ::Any, !Matched::Any, !Matched::Any...) at abstractarray.jl:2065
  push!(!Matched::Array{Any,1}, ::Any) at array.jl:862
  ...
Stacktrace:

WARNING: Be very careful writing exclamation functions

snippet.juliarepl
julia> gextern= [1,2];		## let's see how our h! function will change this external value

julia> h!(G)= (G=[3,4]);	## wrong: G will be replaced by a new G.  the external G=gextern will never see the change

julia> println( h!(gextern) )	## As expected, h!() returns its new [3,4]
[3, 4]

julia> println( gextern )	## ... but h! created (and returned) a local G inside. it did not modify gextern's contents!
[1, 2]

julia> h!(G)= (G.=[3,4]);	## note the dot-prefix .= assignment.  this replaces G's contents, not G itself

julia> h!(gextern); println( gextern )	## presumably what you wanted
[3, 4]

Best-Practice Speed Recommendations for Functions

  • Function calls are resolved at run time, not compile time, eliminating the need for function declarations (before function definitions). Ergo, a function definition is the same as a function declaration.
  • Julia can be extremely fast when run-time takes much longer than compile-time. But even when this is the case, Julia also has oodles of 'gotchas that can slow down execution. In particular, when input or output types can vary, especially at run-time, then the julia compiler can end up having to create swaths of conditional functions, rather than just one function.

Here are my recommendations both to avoid bugs and to speed up programs. Traceur can analyze code for many gotchas.

  • Do not use global variables. (They cannot be typed in 0.6.2, even when defined in modules!) Global constants are ok.
  • Specify the types of all arguments and the type of function return value.
  • Use specific primitive-type arguments and return values when generality is not important — or at least restrict yourself to Real, AbstractFloat, or Integer. Again, this is not only because it can help the compiler and improve speed and memory use, but because this habit will help you find bugs in your program more quickly. Again and again, it is often advisable to define functions with typed arguments that suit their intended use in your programs:

Bad:

snippet.juliarepl
julia> b= 1.0;  function g(a) a+b; end;#function				## Bad

Better:

snippet.juliarepl
julia> const b= 1.0;  function g(a::Real)::Real; a+b; end;#function		## Better

Best:

snippet.juliarepl
julia> const b= 1.0;  function g(a::Float64)::Float64; a+b; end;#function	## Best

(Typing the return value is useful because it helps you catch bugs inside your function. The compiler already knowns that a+b will be Float64. Actually, 0.6.2 has a bug that can occasionally bite, making it even more advisable to type everything.)

  • “Program by contract”: add lots of at the start and end of each function.
snippet.juliarepl
julia> function f( arg1::AbstractFloat )
	    @boundscheck @assert(isfinite(arg1), "first argument '$arg1' should be finite." )
	end;#function##
  • Come to think of it, add many more @asserts in the middle of your code, too.
  • Use @inbounds only after you have debugged your program and are sure that bounds checking is no longer needed.
  • The advice is bendable for really short and simple functions with no risk of misunderstanding: sqr(x)= x^2 is ok. (Strong typing on such a simple function would waste the programmer's time.)

PS: Traceur is a code analyzer that should catch many speed problems. Unfortunately, julia 1.0 (or julialint) do not offer many compiler warnings when meaningful default type variability arises, e.g., due to the use of untyped or undefined variables, global variables, untyped arguments, or untyped return values.

Understanding Passing by Sharing

Function arguments are just new variable bindings:

snippet.juliarepl
julia> f( x::Int )= ( x= 22)
f (generic function with 1 method)

julia> x= 1
1

julia> f(x)
22

julia> x
1

This is similar to other interpreted languages (Python, Ruby, Perl, etc.). FIXME QUESTION Is this like the C++ '&' argument? i.e., f( int& x ), or are there subtle distinctions?

Warning 1: Reassignment to the Binding

snippet.juliarepl
julia> f!( x::Vector{Int} )::Vector{Int}= ( x= [1,2 ] )
f! (generic function with 1 method)

julia> y= [ 1, 2 ]
2-element Array{Int64,1}:
 1
 2

julia> f!( y )				## change y?  assigned to x and returned
2-element Array{Int64,1}:
–12

julia> y == [1,2 ]			## so did our f-bang function change y?
false

julia> y				## no, because x in the f! was assigned to, so it lost its original bindings first
2-element Array{Int64,1}:
 1
 2

FIXME IAW: Fix up the above, so that we abuse the switch to have useless assignments that then do not propagate as we thought they would; and use this as a substitute to copy and deepcopy.

but if the contents of x are changed, the binding of x itself does not change, and

snippet.juliarepl
julia> f!( x::Vector{Int} )::Vector{Int}= ( x[1]= –1; x[2]= –2; x )  ## or use the special dot-equal: x.= [1,2 ]
f! (generic function with 1 method)

julia> y= [ 1, 2 ]
2-element Array{Int64,1}:
 1
 2

julia> f!( y )				## change y
2-element Array{Int64,1}:
–12

julia> y == [1,2 ]			## did our f-bang function change y?
true

julia> y				## no, because x in the f! was assigned to, so it lost its change
2-element Array{Int64,1}:
–12

Warning 1: Assignment Side Effects

A bang! function name should warn the user that the function does surreptitious things to its arguments, but this is just convention. Even a normal function call can do this:

snippet.juliarepl
julia>  add1( x::Vector{Int} )::Vector{Int}= ( result= x .+ 1; (length(x) > 2) && ( x[div(length(x),2)]= –999);  result );

julia> y= [ 1, 2, 3 ]; z= add1( y )		## seems to work
3-element Array{Int64,1}:
 2
 3
 4

julia> y					## excuse me?!
3-element Array{Int64,1}:
 –999
    2
    3

Function Calls with Tuples instead of (Naked) Caller Arguments

Recall that a Tuple is a list of values. Tuples can be thought of as the arguments to the function but without the name of the function. In fact, a function f(a,b) can accept the call f(1,2) or the call tpl=(1,2); f(tpl).

Recall arraysintro for the explanation of tuples. (Tuples are like lists of values.)

snippet.juliarepl
julia> function f1(x::Int, y::AbstractFloat, z::Int8=3); print("$x and $y and $z"); end;##function##

julia> mytuple= ( 2, 3.4, Int8(5) )
(2, 3.4, 5)

julia> typeof(mytuple)
Tuple{Int64,Float64,Int8}

julia> f1( mytuple... )		## note the trailing ...
2 and 3.4 and 5

You can also write functions that treat the tuple as a tuple:

snippet.juliarepl
julia> function f2(x::Tuple); print("the input is $x"); end;##function##

julia> mytuple= ( 2, 3.4, Int8(5) );   f2( mytuple )
the input is (2, 3.4, 5)
  • In f(x; y=0,kwargs...), kwargs passed in should be (key,value) tuples (or be capable of becoming such).

See Tuples for returning multiple values with tuples.

Anonymous Functions

  • Anonymous functions are often passed to map( anonfun, iterator ) and filter( anonfun, iterator ).
  • Anonymous functions cannot specify their return type in the definition. They can however force an unambiguous type in the return, which will still induce the compiler to do the right thing. In any case, arguably, strong typing can be overkill for short anonymous functions, where not only the compiler but also the author can be fairly certain of what is passed into the function and what comes out of it.
snippet.juliarepl
julia> f= function(a, b; c=2); a+b+c ; end#function	## f is variable that holds a function.  the function name is not f!!
7 (generic function with 1 method)

julia> f= function(a::Int, b::Int=2; c::Int=3); a+b+c ; end#function
10 (generic function with 2 methods)

julia> f= function(a::Int, b::Int=2; c::Int=2); Int(a+b+c) ; end#function   ## forces unique return type
13 (generic function with 2 methods)

julia> g= ( (a,b)->a^2+b^2 )
16 (generic function with 1 method)

julia> g= (a::Int64,b::Int64; c::Int64=4) -> a^2+b^2+c^2	## different way of writing function g
18 (generic function with 1 method)
  • Admittedly, in all these examples, the return type can be inferred as Int by the compiler. From a compiler perspective, forcing type is more useful to force the return type when one of the aspects that go into the computation of the return value is an external global variable. From a user perspective, forcing type protects against inadvertent type changes or worse bugs.
  • With ordinary functions that are named (see first subsection), Julia knows that f is a function f(). With anonymous functions (in this subsection) assigned to the variable f, Julia must assume that f could be a function or could be something else. This can reduce the compiler's ability to optimize.

Varargs

See Varargs functions and parametrically constrained varargs.

snippet.juliarepl
julia> f(a,x...)= println("a=$a | x=$x (type of vararg contents=$(typeof(x)))")
f (generic function with 1 method)

julia> f(2,3,4)
a=2 | x=(3, 4) (type of vararg contents=Tuple{Int64,Int64})
  • The x... formal argument collects all caller arguments into a Tuple.
  • The NTuple(N,T) feature makes it possible to require the passing of an Tuples with exactly N components of type T. (Or even at least N units.)

Passing an Array or Tuple to a Varargs function

snippet.juliarepl
julia> f(x...) = sum( collect(x).^2 )          ## collect creates an array from the Tuple held in variable x
f (generic function with 1 method)

julia> f(1,2,3)
14

julia> f( [1,2,3]... )				## here, the ... notation means disassemble the array into a tuple
14

The Vararg Tuple Type

The last parameter of a tuple type can be a Vararg, which denotes trailing elements.

snippet.juliarepl
julia> Mytupletype = Tuple{AbstractString,Vararg{Int}}	## A string, followed by any number of Ints
Tuple{AbstractString,Vararg{Int64,N} where N}

julia> isa( ("1", 1,2,3,4,5) , Mytupletype )		## Matches
true

julia> isa( ("1", 1,2,3,4,5 , "B") , Mytupletype )	## the last item is now a string, not an Int
false
  • See Section 15.5 of Julia's 0.6.2 manual.

Multiple Return Values with Tuples

snippet.juliarepl
julia> f(x) = ( x, x^2, x^4, x^8 )             ## returns tuple
f (generic function with 1 method)

julia> f(2)
(2, 4, 16, 256)

julia> a,b,c,d= f(2); c
16

Tuples for Multiple and/or Mixed-Type Return Values

snippet.juliarepl
julia> function g(x::Int)::Tuple{Int64,Int64}; rv= (1,2); @info(typeof(rv)); rv end#function
g (generic function with 1 method)

julia> g(2)
[ Info: Tuple{Int64,Int64}
(1, 2)

julia> function h(x::Int)::NTuple{2,Int64}; rv= (1,2); @info(typeof(rv)); rv end#function
h (generic function with 1 method)

julia> h(2)
[ Info: Tuple{Int64,Int64}
(1, 2)
  • WARNING Plain instead of curly parentheses as arguments to the NTuple make for baffling errors

Ignoring Part of the Return (Tuple)

snippet.juliarepl
julia> using Random

julia> Random.seed!(0);

julia> A= rand(3,3)
3×3 Array{Float64,2}:
 0.823648  0.177329  0.0423017
 0.910357  0.27888   0.0682693
 0.164566  0.203477  0.361828

julia> using LinearAlgebra

julia> (Q,_) = qr(A);              ## random orthogonalized matrix only wanted. qr returns two matrices, but _ ignores return

julia> Q
3×3 LinearAlgebra.QRCompactWYQ{Float64,Array{Float64,2}}:
 –0.664962   0.329743   0.6701460.7349650.1292830.6656670.132860.935177   0.328318

Function Documentation

Strings preceding functions are considered documentation. (Suggestion: Use triple double-quotes.) Because the format is markdown, it allows sectioning the documentation. (Use double backquotes for LaTeX.)

snippet.julianoeval
[download only julia statements]
"""
    fdocumented(x[, y])
 
The one-liner explanation.  Four spaces before the function name above.
 
 
# Arguments
 
- `x::Vector{Int}` : a vector of integers
- `y::Vector{Int}` : a vector of more integers
 
 
# Details
 
we do nothing, but we do it well.
 
# Examples
 
\```
 julia> fdocumented( [1,2] )
 [1,2]
\```
 
"""
fdocumented(x)=(x)
fdocumented(x,y)=(x,y)

and now the help browser is available:

snippet.text
[download only julia statements]
help?> fdocumented
search:
 
  fdocumented(x[, y])
 
  The one-liner explanation. Four spaces before the function name above.
 
     Arguments
    ≡≡≡≡≡≡≡≡≡≡≡
 
    •    x::Vector{Int} : a vector of integers
 
    •    y::Vector{Int} : a vector of more integers
 
 
     Details
    ≡≡≡≡≡≡≡≡≡
 
  we do nothing, but we do it well.
 
     Examples
    ≡≡≡≡≡≡≡≡≡≡
 
  julia> fdocumented( [1,2] )
   [1,2]

Checking Documentation

Persistent State (Local Static Variables)

Let Clutch

There is an ugly clutch facility (instead of the much nicer static C keyword), which works because functions declared inside a let block are global:

snippet.juliarepl
julia> let state= 0;
   global counter;
   function counter() state+=1; end#function counter
   state
   end#let##
0

julia> counter(); counter(); counter()
3

Channels

An often feasible and more elegant alternative to a state (ahem, let) variable is a channel:

snippet.juliarepl
julia> function counter(c::Channel)::Int;
                ## don't worry---the following will not fill the channel, but return after each put!
                for state=1:typemax(Int); put!(c, state); end#for
	end;#function##

julia> nn= Channel(counter);

julia> take!(nn); take!(nn); take!(nn)
3

julia> close(nn)

Global Variables (and Changing Globals Inside Functions)

Global variables cannot be typed. Every program can see global variables, but they are by default read-only. This can be changed with the global keyword, but should be strictly avoided.

snippet.juliarepl
julia> gx= 1;  function f() global gx= 2; end;##function

julia> f(); gx
2
  • Nested functions have write access to local variables one scope up (unless the scope-up is global).

Introspection (Function Name, Content, etc.)

FIXME mention @macro facility for knowing more about a function.

snippet.juliarepl
julia> function a() end#function
a (generic function with 1 method)

julia> x= a
a (generic function with 1 method)

julia> Symbol(x)
:a
  • Unfortunately, typing the name of the function does not print its source code, as it does in R. In fact, the function source seems to be discarded (irretrievable in 0.6.2).

Chaining Function Calls (Syntactic Sugar)

snippet.juliarepl
julia> inv( sum( ([1:5;]).^2 ) )   ## watch the trailing semi-colon for array
0.01818181818181818

Already mentioned in control flow, Julia offers “unix-like-piping” way to highlight sequencing for sequential (nested) function calls.

snippet.juliarepl
julia> [1:5;] |> x->x.^2 |> sum |> inv           ## x->x.^2 is an anonymous function
0.01818181818181818

This becomes less convenient (requiring an anonymous function) when chained functions have more than one argument.

snippet.juliarepl
julia> gnep(needle,heystack) = filter( x->occursin(x, needle), heystack );

julia> ss= [ "ab1", "ab2", "cd1", "ab3", "ef5" ];

julia> ( gnep("AB", uppercase.(ss)) )  ==  ( uppercase.(ss) |>  x->gnep( "AB", x) )
true

Wrapping a Function Call Around Another Function ("Hooking Into a Function")

snippet.juliarepl
julia> function wrapfun( funin::Function )::Function
	    gallcount= 0; gfun= funin;
            function fundispatcher( x... );  gallcount+= 1; gfun( x... );  end#function## add 1 and return the original function
            function fundispatcher()::Int;  @info("Number of Function Calls: $gallcount"); gallcount;  end#function## give info
	    return fundispatcher	## return the wrapper function
	end;#function##

julia> mysqr= wrapfun( x->x^2 );	## x^2 is an anonymous function that squares its contents

julia> @assert( sum( map( x->mysqr(x), 1:20 ) ) == 2870, "sum is wrong" )	## run the function 20 times

julia> mysqr( )			## call the info function
[ Info: Number of Function Calls: 20
20

Argument Dependencies in Default Arguments

Arguments are parsed left to right. Later arguments can use values of earlier arguments.

snippet.juliarepl
julia> b= 9999;

julia> lrfun( a, b=1, c=b )= println("$a $b $c");		## ok

julia> lrfun( 12 )
12 1 1

julia> rlfun( a, c=b, b=1)= println("$a $b $c" );		## left-to-right means b is the global one before arg3

julia> rlfun( 12 )
12 1 9999

Writing Macros

FIXME Show more macro examples

Macros (like C Preprocessor #define functions) can be useful to do meta programming. Avoid them unless you need meta programming—although the following examples just illustrate them.

snippet.juliarepl
julia> mutable struct Point; x::Float64; f::Float64; end#struct;##

julia> module M
	macro testme(newp)
	    esc(:($newp.f <= 0.0 ? string($newp, " below") : string($newp, " above")))
	end#macro#
	end#module##
Main.M

julia> macroexpand(M, quote @testme p end)	## how to show what it expands into
quote
    #= none:1 =#
    if p.f <= 0.0
        string(p, " below")
    else
        string(p, " above")
    end
end

julia> @M.testme(Point( 1.0, 2.0 ))
"Point(1.0, 2.0) above"

Macro Hygiene

FIXME Explain Macro Hygiene with an example

Backmatter

Useful Packages on Julia Repository

Notes

  • In Computer Science, sometimes the arguments (parameters) in the function definition are named “formal parameters,” and the arguments (parameters) in the caller are named “actual parameters.” I wish the jargon was clearer.
  • Unfortunately, there are no elegant C-type static variables inside functions.
  • Unfortunately, it is not possible to turn on a compiler warning whenever meaningful default type variability arises, e.g., due to global variables or untyped arguments. Use Traceur.
  • Julia does not forward-scan .jl source files to collect (forward references for) functions declared later, even in batch mode where a pre-scan would be a cheap fallback. Thus, programmers typically define their least-important deepest utility functions first. perl has a more programmer-friendly fallback here that scans forward, while admitting imperfection (it won't always work). Forward-scanning would not be desirable forced behavior, but desirable with a pragma option.
  • There are all sorts of ambiguities that can arise with abstract containers. Be specific (and/or read 15.9 of the Julia 0.6.2 manual).
  • Fancier dynamic meta-programming documentation creation is possible. (Chapter 20, Julia 0.6.2)
funother.txt · Last modified: 2018/12/05 19:46 (external edit)