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

constant

A thunk which returns the same value every time.

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.