5.2. Functions¶
This chapter describes types of functions, and construction of functions.
5.2.1. Types of functions¶
5.2.1.1. Methods¶
A method is a function stored in a value.
- method¶
A function which is stored in a variable of a value, and used to do an operation for the value as the receiver.
- receiver¶
The value which has a method.
Assume you make a single-element immutable container type just
,
which has one method: get
.
This type can be defined and used as follows.
:new_just <- {(:Val)
new_val(
... Just_trait
'Val' Val
)
}
:Just_trait <- [
# [1]
'get' {[:J]()
J.Val
}
]
:Just <- new_just(42)
# [2]
:V <- Just.get
stdout.print_line(V.repr) # => 42
get
method is called at [2].
The receiver Just
is passed to J
in the implementation of get
at [1].
5.2.1.2. Constructors¶
A value is usually created by a function called constructor.
- constructor¶
A function which creates a new value.
Usually, a constructor is named new
.
For example, suppose a module named example/RAT
provides functionality of
rational numbers.
In example/RAT
mod, a constructor of a rational number can be defined as follows:
# example/RAT.kn
:new <- {(:Numer :Denom)
new_val(
'numer' {[:R]() R.Numer }
'denom' {[:R]() R.Denom }
'repr' {[:R]()
'(rat numer={} denom={})'.format(R.numer.repr R.denom.repr)
}
'Numer' Numer
'Denom' Denom
)
}
This constructor can be called from a client of the mod as follows:
:RAT.require_from('example/')
stdout.print_line(RAT.new(2 5).repr) # => (rat numer=2 denom=5)
stdout.print_line(RAT.new(3 7).repr) # => (rat numer=3 denom=7)
Apparently, numer
, denom
, and repr
methods can be shared among
rational number values.
Thus, usually, methods and the syms are stored in a vec called a trait,
then they are spreaded as arguments of new_val
.
- trait¶
A vec which stores methods and the syms, which are spreaded as arguments of
new_val
.
For example, example/RAT
mod can be rewritten as follows,
using a trait Rat_trait
.
# example/RAT.kn
:new <- {(:Numer :Denom)
new_val(
... Rat_trait
'Numer' Numer
'Denom' Denom
)
}
:Rat_trait <- [
'numer' {[:R]() R.Numer }
'denom' {[:R]() R.Denom }
'repr' {[:R]()
'(rat numer={} denom={})'.format(R.numer.repr R.denom.repr)
}
]
5.2.1.3. Thunks¶
- thunk¶
A function which does not take an argument.
This is a thunk:
{() stdout.print_line('hello') }
This is also a thunk:
{ stdout.print_line('hello') }
A thunk is used to delay evaluation until its result or side-effect is needed.
In the example below,
only one of two thunks negative_cont
and non_negative_cont
is called based on the sign of Num
.
:branch_on_sign <- {(:Num :negative_cont :non_negative_cont)
if(Num < 0
{ negative_cont }
{ non_negative_cont }
)
}
# => negative
branch_on_sign(
-1
{ stdout.print_line('negative') }
{ stdout.print_line('non negative') }
)
# => non negative
branch_on_sign(
1
{ stdout.print_line('negative') }
{ stdout.print_line('non negative') }
)
5.2.1.4. Constants¶
A constant can be made like this:
:Result_of_heavy_calculation <- 42
# constant which returns 42
:answer <- {() Result_of_heavy_calculation }
stdout.print_line(answer.repr) # => 42
See LOCALE.root of kink/LOCALE mod and TRACE.snip of kink/TRACE mod for examples in the builtin API.
Note
The reason of providing constants as functions is to maintain a principle that only functions can be public, while data variables cannot. See Accessibility.
5.2.2. Receiver and parameters¶
5.2.2.1. Optional and variadic parameters¶
If you want to take optional arguments after mandatory ones,
you can use Varref.opt
as follows.
:posix_locale_tag <- {(:Lang :Opt_territory.opt :Opt_charset.opt)
:Territory = Opt_territory.just_or{ 'US' }
:Charset = Opt_charset.just_or{ 'UTF-8' }
stdout.print_line('{}-{}.{}'.format(Lang Territory Charset))
}
posix_locale_tag('en' 'GB' 'ASCII') # => en-GB.ASCII
posix_locale_tag('en' 'GB') # => en-GB.UTF-8
posix_locale_tag('en') # => en-US.UTF-8
As shown above, if an argument is given to the optional parameter, a single element vec containing the argument is passed to the variable. If no argument is given to the optional parameter, an empty vec is passed to the variable.
See kink/param/OPT_PARAM for details.
If you want to take variadic arguments at the end of the arguments list,
you can use Varref.rest
as follows.
:commandline <- {(:Command :Args.rest)
stdout.prine_line('command: {}'.format(Command))
Args.size.times{(:I)
stdout.prine_line('arg #{}: {}'.format(I Args.get(I)))
}
}
commandline('ls' '-ltr' '/etc')
# Output:
# command: ls
# arg #0: -ltr
# arg #1: /etc
See kink/param/REST_PARAM for details.
You can take both optional and variadic arguments.
:take_opt_variadic <- {(:Mandatory :Opt.opt :Rest.rest)
stdout.prine_line('Mandatory {}'.format(Mandatory.repr))
stdout.prine_line('Opt {}'.format(Opt.repr))
stdout.prine_line('Rest {}'.format(Rest.repr))
}
take_opt_variadic('foo' 'bar' 'baz' 'qux')
# Output:
# Mandatory "foo"
# Opt ["bar"]
# Rest ["baz" "qux"]
5.2.2.2. Config functions¶
When positional optional parameters are not flexible enough, a config function can be used.
For example,
PROGRAM.compile (see kink/PROGRAM) can take
a binding, a locale, a program name, and other optional values,
and each can be omitted independently.
For that case, optional parameters are not appropriate.
Assuming that the parameter list is
(Text, opt Binding, opt Locale, opt Program_name)
,
you cannot omit only Binding
,
while specifying Locale
and Program_name
.
So, PROGRAM.compile takes an optional config function at the end of the parameter list. The client of PROGRAM.compile can specify a binding, a locale, and a program name, by calling methods of a config val, which is passed to the config function.
:PROGRAM.require_from('kink/')
:LOCALE.require_from('kink/')
:BINDING.require_from('kink/')
:print_num <- PROGRAM.compile('stdout.print_line(Num.repr)'){(:C)
:Binding = BINDING.new
Binding:Num <- 42
C.binding(Binding)
}
print_num # => 42
:zero_division <- PROGRAM.compile('10 // 0'){(:C)
C.name('calc.kn')
C.locale(LOCALE.for('en'))
}
zero_division
# Output:
# -- main exception
# [(root)]
# ,,,
# {calc.kn L1 C4 op_intdiv} 10 -->// 0
# Num.op_intdiv(Divisor): zero division: 10 is divided by 0
Here, the last parameter of PROGRAM.compile is a config function.
The parameter C
of the config function is a config value.
If you provide a function which takes a config function, and the config value is simple enough, you can use kink/CONFIG_FUN_RUNNER.
Note
Config functions can be seen as an alternative aproach to named parameters of other languages. The most notable advantage of config functions is that they don't need a dedicated syntax. A config function is just the last argument passed to the configured function.
5.2.3. Function result¶
5.2.3.1. Return value¶
In most cases, a function returns a value, which is the value of the last expression of the sequence of the function body.
Example:
:NUM.require_from('kink/')
:fraction_part <- {(:Num)
NUM.is?(Num) || raise('Num must be a num, but was {}'.format(Num.repr))
Num % 1
}
stdout.print_line(fraction_part(3.1415).repr) # => 0.1415
In the example above, fraction_part
function
returns the result value of Num % 1
as its result.
5.2.3.2. Continuation¶
Sometimes a function call results in calling another function at the tail. Such a function called at the tail is called a continuation.
- continuation¶
A function which is called at the tail of a function call. Don't confuse with delimited continuation.
- continuation passing style¶
Composition of a function where the continuation is passed from the caller.
For example, with_just_or($cont_just $cont_empty)
method
of a vector
is written in continuation passing style.
It calls cont_just
at the tail when the size is 1,
or calls cont_empty
at the tail when the size is 0.
Thus, cont_just
or cont_empty
is the continuation
of with_just_or
.
Example:
:display_opt <- {(:Opt_vec)
Opt_vec.with_just_or(
{(:Just) 'just {}'.format(Just.repr) }
{ 'empty' }
)
}
stdout.print_line(display_opt([])) # => empty
stdout.print_line(display_opt([42])) # => just 42
The main loop of an application is often times constructed on continuation passing style. Thus, it is essentially important to make the continuation called as a tail call, so that the main loop does not result in stack overflow.
5.2.3.3. Exception¶
A function call can terminate raising an exception, when there is no other way.
For example below, fraction_part
requires a number as the argument.
If that expectation is not met,
fraction_part
raises an exception because the precondition is broken.
:NUM.require_from('kink/')
:fraction_part <- {(:Num)
NUM.is?(Num) || raise('Num must be a num, but was {}'.format(Num.repr))
Num % 1
}
fraction_part('str')
# Output:
# -- main exception
# [(root)]
# {(call by host)}
# {(call by host)}
# {startup}
# {builtin:kink-mods/kink/_startup/STARTUP.kn L238 C3 _startup_aux} -->_startup_aux(Args Dep)
# {builtin:kink-mods/kink/_startup/STARTUP.kn L221 C11 try} CONTROL.-->try(
# [builtin:kink-mods/kink/CONTROL.kn L93 C33 reset] :switch = KONT_TAG.escape_tag.-->reset{
# {(call by host)}
# [(kont tag)]
# [builtin:kink-mods/kink/CONTROL.kn L94 C10 body] :R = -->body
# {builtin:kink-mods/kink/_startup/STARTUP.kn L223 C7 _start} -->_start(Non_opts Dep)
# {builtin:kink-mods/kink/_startup/STARTUP.kn L120 C3 if} -->if(Non_opts.empty?
# {(call by host)}
# {builtin:kink-mods/kink/_startup/STARTUP.kn L131 C20 call} { :Source_spec -->= Non_opts.front
# {builtin:kink-mods/kink/_startup/STARTUP.kn L132 C20 call} :Script_args -->= Non_opts.drop_front(1)
# {builtin:kink-mods/kink/_startup/STARTUP.kn L133 C7 branch} -->branch(
# {(call by host)}
# {builtin:kink-mods/kink/_startup/STARTUP.kn L158 C34 call} [:Source_desc :Script] -->= _scan_from(Source_spec Dep)
# {builtin:kink-mods/kink/_startup/STARTUP.kn L160 C20 call} :Binding -->= BINDING.new
# {builtin:kink-mods/kink/_startup/STARTUP.kn L162 C11 _run_script} -->_run_script(Binding $script_fun Script_args)
# [builtin:kink-mods/kink/_startup/STARTUP.kn L111 C3 script_fun] -->script_fun
# {(stdin) L8 C1 fraction_part} -->fraction_part('str') # => Num must be a num, but was "str"
# [(stdin) L4 C16 op_logor] NUM.is?(Num) -->|| raise('Num must be a num, but was {}'.format(Num.repr))
# {(call by host)}
# {(stdin) L4 C19 raise} NUM.is?(Num) || -->raise('Num must be a num, but was {}'.format(Num.repr))
# Num must be a num, but was "str"
See Error handling for details.
5.2.3.4. Side effects¶
Function might also cause side effects, such as changing the target value of a member variable, or I/O to the file system or the network.
In the next example,
fill_zero
sets 0 to all the elements of a vector.
It is considered as a side effect of fill_zero
.
:fill_zero <- {(:Vec)
Vec.size.times.each{(:I)
Vec.set(I 0)
}
}
:Vec <- [1 2 3 4 5]
fill_zero(Vec)
stdout.print_line(Vec.repr) # => [0 0 0 0 0]
It is better to avoid side effects as much as possible, unless side effects themselves are the purpose of the function.
Here is a caveat. In mainstream languages, modification of a value not exposed from the function is not considered as a side effect. However, in Kink, that can sometimes be an observable side effect because of delimited continuation.
In the following example,
you might think Mapped
is not exposed from map
function,
so calling push_back
method does not cause side effects.
:map <- {(:Vec :transform)
:Mapped = []
:loop <- {(:I)
if(I < Vec.size){
Mapped.push_back(transform(Vec.get(I)))
loop(I + 1)
}
}
loop(0)
Mapped.dup
}
:Double <- map(
[0 1 2 3]
{(:I) I * 2 }
)
stdout.print_line(Double.repr) # => [0 2 4 6]
However, it is possible that a function call of map
is resumed from the middle.
The following example shows that modification of Mapped
can actually be observed from outside.
:KONT_TAG.require_from('kink/')
:double_caller <- {(:Vec)
:Tag = KONT_TAG.new
Tag.reset{
map(Vec){(:Num)
if(Num == 0
{ Tag.shift{(:resume)
{ resume(nada) }
}
0
}
{ Num * 2 }
)
}
}
}
:call_double <- double_caller([0 1 2 3])
:D1 <- call_double
stdout.print_line(D1.repr) # => [0 2 4 6]
:D2 <- call_double
stdout.print_line(D2.repr) # => [0 2 4 6 0 2 4 6]
To avoid this kind of side effect,
you can define map
like the following.
:map <- {(:Vec :transform)
:loop <- {(:I :Mapped)
if(I < Vec.size
{ :New_mapped = Mapped + [transform(Vec.get(I))]
loop(I + 1 New_mapped)
}
{ Mapped }
)
}
loop(0 [])
}
Also note that implementation with side effects can sometimes be
justified from the point of view of efficiency.
Possibly it can be faster, or it can allocate less space.
For example, the builtin implementation of map
method
a vector can cause side effects
when it is resumed by delimited continuation in the middle.