7.2. 関数

この章は 関数 の種類と、関数の構成について述べる。

7.2.1. 関数の種類

7.2.1.1. Methods

A method is a function stored in a value, and used to do an operation for the value as the receiver.

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].

7.2.1.2. Constructors

A constructor is 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.

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)
  }
]

7.2.1.3. Thunks

A thunk is a function that is not used as a method, and takes no arguments.

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') }
)

7.2.1.4. 述語

述語 とは、メソッドして使われず、引数をひとつ取り、ブール値を戻すような関数である。

たとえば、次の関数は述語だ。

{(:N) N % == 2 }

述語は Vec.filter のようなメソッドに使われる。

7.2.1.5. Constants

A constant is a thunk which returns a 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.

注釈

The reason of providing constants as functions is to maintain a principle that only functions can be public, while data variables cannot. See Accessibility.

7.2.2. 呼び出し元から渡される値

関数の呼び出し元から渡される値の基本については、Formal receiver and formal argumentsを見よう。この節では、呼び出し元から渡される値の高度な取り扱いについて述べる。

7.2.2.1. オプショナルパラメータと任意長パラメータ列

必須の引数のあとにオプショナルな引数を取りたい場合、次のように Varref.opt が使える。

:posix_locale_tag <- {(:Lang :Opt_territory.opt :Opt_charset.opt)
  :Territory = Opt_territory.just{ 'US' }
  :Charset = Opt_charset.just{ '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

上記のように、オプショナルパラメータに対して引数が与えられた場合、その引数を含む一要素のベクタが変数に格納される。オプショナルパラメータに対して引数が与えられない場合、空のベクタが変数に格納される。詳細は OPT_PARAM を見よう。

引数列の末尾で任意の数の引数を取りたい場合、次のように Varref.rest が使える。

:commandline <- {(:Command :Args.rest)
  stdout.print_line('command: {}'.format(Command))
  Args.size.times.each{(:I)
    stdout.print_line('arg #{}: {}'.format(I Args.get(I)))
  }
}

commandline('ls' '-ltr' '/etc')
# Output:
#   command: ls
#   arg #0: -ltr
#   arg #1: /etc

詳細は REST_PARAM を見よう。

Varref.optとVarref.restは同時に使える。

:take_opt_variadic <- {(:Mandatory :Opt.opt :Rest.rest)
  stdout.print_line('Mandatory {}'.format(Mandatory.repr))
  stdout.print_line('Opt {}'.format(Opt.repr))
  stdout.print_line('Rest {}'.format(Rest.repr))
}

take_opt_variadic('foo' 'bar' 'baz' 'qux')
# Output:
#   Mandatory "foo"
#   Opt ["bar"]
#   Rest ["baz" "qux"]

7.2.2.2. コンフィグ関数

オプショナルパラメータよりも柔軟な方法として、コンフィグ関数が使える。

たとえば、 Program.compile は、束縛ロケール、その他オプショナルな値を取る。これらの値は、それぞれ独立に省略できる。

これを実現するため、Program.compileは引数列の末尾で、オプショナルなコンフィグ関数を取る。Program.compileの呼び出し元は、束縛、ロケール、その他のオプショナルな値を、コンフィグ関数に渡されるコンフィグ値のメソッドを呼ぶことで指定する。コンフィグ値のメソッドはコンフィグメソッドと呼ばれる。

:LOCALE.require_from('kink/')
:BINDING.require_from('kink/')
:PROGRAM.require_from('kink/program/')

:Program <- PROGRAM.new('printnum.kn' 'stdout.print_line(Num.repr)')
:print_num <- Program.compile{(:C)
  :Binding = BINDING.new
  Binding:Num <- 42
  C.binding(Binding)
}
print_num # => 42

ここで、Program.compileの引数はコンフィグ関数である。コンフィグ関数のパラメータ C はコンフィグ値である。

典型的なコンフィグ値を取るコンフィグ関数を走らせるには、 CONFIG_FUN_RUNNER.new が使える。

注釈

コンフィグ関数は、他の言語における名前付きパラメータと同じように機能する。

7.2.3. Function result

7.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.

7.2.3.2. 継続

関数呼び出しは、他の関数を 末尾呼び出し することがある。このような、末尾で呼び出される関数は、 継続 と呼ばれる。 限定継続 と混同しないように。

プログラムが、呼び出し元から継続が渡されるようなやり方で書かれている場合、そのようなプログラムは 継続渡しスタイル で書かれている、と呼ばれる。

たとえば、 Vec.with_elems は継続呼び出しスタイルで書かれている。 with_elems は引数として渡された関数を末尾呼び出しする。したがって、この関数は with_elems の継続である。例:

:display_opt <- {(:Opt_vec)
  Opt_vec.with_elems(
    { 'empty' }
    {(:Just) 'just {}'.format(Just.repr) }
  )
}

stdout.print_line(display_opt([]))  # => empty
stdout.print_line(display_opt([42]))  # => just 42

7.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 L260 C3 _startup_aux} -->_startup_aux(Args Dep)
#   {builtin:kink-mods/kink/_startup/STARTUP.kn L243 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 L245 C7 _start} -->_start(Non_opts Dep)
#   {builtin:kink-mods/kink/_startup/STARTUP.kn L137 C3 if} -->if(Non_opts == []
#   {..call by host..}
#   {builtin:kink-mods/kink/_startup/STARTUP.kn L152 C20 call} { :Source_spec -->= Non_opts.front
#   {builtin:kink-mods/kink/_startup/STARTUP.kn L153 C20 call} :Script_args -->= Non_opts.drop_front(1)
#   {builtin:kink-mods/kink/_startup/STARTUP.kn L154 C7 branch} -->branch(
#   {..call by host..}
#   {builtin:kink-mods/kink/_startup/STARTUP.kn L180 C34 call} [:Source_desc :Script] -->= _scan_from(Source_spec Dep)
#   {builtin:kink-mods/kink/_startup/STARTUP.kn L182 C20 call} :Binding -->= BINDING.new
#   {builtin:kink-mods/kink/_startup/STARTUP.kn L184 C11 _run_script} -->_run_script(Binding $script_fun Script_args)
#   [builtin:kink-mods/kink/_startup/STARTUP.kn L128 C3 script_fun] -->script_fun
#   {(stdin) L8 C1 fraction_part} -->fraction_part('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.

7.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.