Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Анонимные функции (FuncN, Func)

Функциональные возможности

Libretto поддерживает базовые функциональные возможности. Можно определять анонимные функции с замыканием. Такие анонимные функции являются значениями, которые можно передавать в другие методы или функции (высших порядков), возвращать как результат.

Трейты FuncN и Func

Анонимная функция - это код, который можно вызывать по требованию (лениво), передавая нужные аргументы и получая результат.

В Libretto анонимные функции определяются трейтами FuncN из базового пакета libretto.

Анонимная функция без параметров, но возвращает значение:

trait Func0[R] {
  def `!`: R
}

Анонимная функция с одним параметром:

trait Func1[P0, R] {
  def `!`(p0: P0): R
}

Анонимная функция с двумя параметрами:

trait Func2[P0, P1, R] {
  def `!`(p0: P0, p1: P1): R
}

И т.д.

Общее правило: Анонимная функция определяется трейтом с именем FuncN, где N - это число параметров анонимной функции (от 0 включительно). Сам же трейт параметризуется типами в количестве N + 1. Первые N типов - это последовательное перечисление типов параметров, последний тип - это всегда тип результата. Например:

Func0[Int] - анонимная функция, которая не принимает параметры и возвращает Int

Func3[Int, Int, String, String*] - анонимная функция, которая принимает три параметра (типов Int, Int, String) и возвращает тип String*.

Для запоминания: в обычном определении def f(a: A, b: B): C тип результата всегда последний, так и в в определении трейта для анонимной функции результат тоже всегда последний параметрический тип.

Для удобства, чтобы не считать число параметров, есть универсальный трейт-псевдоним Func (тоже из пакета libretto), который работает как FuncN, но без указания числа параметров. Количество параметров анонимной функции подсчитает сам компилятор.

Func[Int] - это псевдоним Func0[Int]

Func[Int, Int, String, String*] - это псевдоним Func3[Int, Int, String, String*]

Во всех трейтах Func/FuncN определен только один метод !, который и служит для вызова кода анонимной функции. ! здесь и во всём Libretto имеет смысл apply (применить).

При определении трейта имя метода ! оборачивается в обратные кавычки как `!`, но при вызове метода можно использовать имя без кавычек: !.

Определение анонимной функции

Поскольку анонимная функция - это трейт с одним методом !, то можно определить конкретный экземпляр при помощи обычного способа создания экземпляра трейта:

def printValue(f: Func[String]) = {
  println("printValue")
  println(f.!)
}

def main = {
  fix f = new Func[String] {
    def `!` = "Hello"
  }
  printValue(f)
}

Выдаёт:

printValue
Hello

Естественно, допустимо замыкание:

def printValue(f: Func[String]) = {
  println("printValue")
  println(f.!)
}

def createFunc(name: String): Func[String] = {
  new Func[String] {
    def `!` = "Hello, " + name
  }  
}

def main = {
  fix name = "world"
  fix f = createFunc(name)
  printValue(f)
}

Выдаёт:

printValue
Hello, world

Такое определение вполне возможно и не противоречит духу Libretto, но оно достаточно громоздкое. Для более простого определения используется встроенная функция func из пакета libretto и #-выражение, которое передаётся единственным аргументом в func.

Определение анонимной функции без параметров при помощи func и #{ ... } с использованием замыкания:

def printValue(f: Func[String]) = {
  println("printValue")
  println(f.!)
}

def main = {
  fix name = "world"
  fix f = func: #{ "Hello, " + name }
  printValue(f)
}

Выдаёт:

printValue
Hello, world

Определение анонимной функции с двумя параметрами:

def printValue(f: Func[Int, Int, Int]) = {
  println("result for 1 and 2: " + f.!(1, 2))
}

def main = {
  fix f = func: #(a: Int, b: Int) { a + b }
  printValue(f)
}

Выдаёт:

result for 1 and 2: 3

Общий синтаксис #-выражения: #(параметры, если есть){ выражение }

Параметры перечисляются в таком же виде, как в обычном методе (с обязательным указанием типа). Но если параметров нет, то круглые скобки не ставятся. Выражение (тело анонимной функции) является обычным выражением Libretto и может занимать несколько строчек и определять переменные (поскольку является частью {}-выражения):

def printValue(f: Func[Int, Int, String, String]) = { 
  println("result for 1, 2, \"@\": " + f.!(1, 2, "@"))
}

def main = {
  fix f = func: #(a: Int, b: Int, suffix: String) {  
    fix a100 = a + 100
    fix b100 = b + 100
    a100.string + b100.string + suffix
  }
  printValue(f)
}

Выдаёт:

result for 1, 2, "@": 101102@

Между # и () пробел не ставится. Между круглыми скобами () и фигурными {} обычно принято ставить пробел. Но если круглых скобок () нет (анонимная функция без параметров), то между # и фигурными скобкаи {} пробел не ставится.

Вызов func возник по историческим причинам, поскольку #-выражение использовались для определения устаревшей “лямбды”. Актуальные анонимные функции появились позднее и требовали какого-то отличия, чем и стал вызов func. В будущем возможен полный отказ от func. Кроме того, в некоторых случаях можно func уже не указывать - см. ниже описание таких случаев.

В целом общее правило: как тип использовать Func или FuncN, а для определения использовать #-выражение с вызовом func (который можно убрать в некоторых случаях).

Замыкание

Как и при создании экземпляра трейта, при определении анонимной функции можно использовать замыкание:

def create(a: Func[Int]): Func[Int, Int] = {
  func: #(b: Int){ a.! + b }
}

def main = {
  fix num = 123
  fix f = func: #{ num }
  fix sumF = create(f)
  println(sumF.!(777))
}

Выдаёт:

900

В том числе и var для замыкания:

def addAll(bufferFunc: Func[Int, ()]) = {
  0..3 as i.${ bufferFunc.!(i) }
  ()
}

def main = {
  var buffer: Int* = ()
  fix f = func: #(v: Int){ buffer += v }
  addAll(f)
  println(buffer)
}

Выдаёт:

(0,1,2,3)

Типизация результата

При использовании трейта Func/FuncN нужно явно указывать тип результата (последним параметрическим типом). При использовании func и #-выражения вывод типа результата производится автоматически:

def main = {
  fix f = func: #(x: Int) { x + "abc" }
  println: type(f)
}

Выдаёт

libretto/Func1[libretto/Int,libretto/String]

Но иногда надо указать тип руками (например, менее специфичный). Для этого нужно преобразовать результат явно в нужный тип. Например, при помощи fix с указанием типа:

def main = {
  fix f = func: #(x: Int) { 
    fix ret: (Int | String) = x + "abc"
    ret
  }
  println: type(f)
}

Выдаёт:

libretto/Func1[libretto/Int,(libretto/Int | libretto/String)]

Либо при помощи upcast:

def main = {
  fix f = func: #(x: Int) { 
    upcast[(Int | String)]: x + "abc"
  }
  println: type(f)
}

Выдаёт:

libretto/Func1[libretto/Int,(libretto/Int | libretto/String)]

Если результат не нужен, то можно использовать ():

def work(f: Func[()]) = {
  f.!
}

def main = {
  fix f = func: #{ println("Hello"); () }
  work(f)
}

Выдаёт

Hello

$, this, return

Например, два эквивалентных определения:

fix f = new Func[String] {
  def `!` = expr1
}

и

fix f = func: #{ expr2 }

В обоих определениях стартовым контекстом $ для выражения является unit.

А вот this работает по-разному.

В трейте допускается thisexpr1), что будет означать сам экземпляр трейта. Это позволяет использовать рекурсию:

def main = {
  fix fact = new Func[Int, Int] {
    def `!`(n: Int): Int = { if (n < 2) 1 else n * this.!(n-1) }
  }
  1..5 as i.${
    println: i+"! = "+fact.!(i)
  }
}

Выдаёт:

1! = 1
2! = 2
3! = 6
4! = 24
5! = 120

А вот #-выражение не позволяет использовать thisexpr2), это вызывает ошибку компиляции.

Проблема в том, что будет конфликт: this может восприниматься и как this со значением самой анонимной функции, и как внешний this для замыкания:

def Int test = func: #{ println(this) } - здесь this какого типа?

Кроме того, использование this для обозначения самой анонимной функции (как в создании экземпляра трейта) требует знания типа результата этой анонимной функции (для типизации Func/FuncN), а он будет выведен только после компиляции тела анонимной функции, где этот this и используется.

Есть важное отличие и при использовании return.

В первом определении (через трейт) компилятор допускает использование returnexpr1), который делает возврат из определяемого метода `!`.

Во втором определении (через func и #) компилятор полностью запрещает использование returnexpr2). По аналогичной с this причине. return может относиться как к самой анонимной функции, так и к внешней функции или методу:

def test: Int = func: #{ return 123 } - это возврат из анонимной функции (как при создании экземпляра трейта) или из метода test?

И опять же для return надо знать тип результата анонимной функции, а он выводится только после компиляции тела, где этот return и используется.

Ещё стоит отметить, что в будущем return может быть ограничен в использовании, поэтому уже сейчас предпочтительно либо стараться не использовать return, либо (для нелокальных возвратов) использовать связку breaking и break (todo).

Вызов анонимной функции

Поскольку анонимная функция представлена трейтом с единственным методом !, то для вызова он же и используется обычным способом через точку .. Особенность в том, что при вызове имя ! не нужно оборачивать в обратные скобки (в отличие от определения метода с таким именем).

Если параметров нет, то вызывается метод ! без скобок. Если параметры есть, то они передаются в скобках. В целом ! ведёт себя полностью как обычный метод (с анонимной функцией в контексте), но со специфическим именем.

def main = {
  fix f0 = func: #{ "f0" }
  fix f2 = func: #(a: Int, b: Int) { "f2: "+ (a + b) }
  println: f0.!
  println: f2.!(1, 2)
  println: f2.!(1): 2
}

Выдаёт:

f0
f2: 3
f2: 3

Метод ! во всём Libretto имеет смысл apply (применить).

Без func

Если анонимная функция определяется при помощи #-выражения непосредственно как аргумент параметра типа FuncN/Func при вызове метода, то можно не использовать вызов func:

def work(f: Func0[String]) = {
  println(f.!)
}

def main = {
  fix f = func: #{ "a" } // func нужен
  work(f)
  work(#{ "b" }) // можно без func
  work(func: #{ "c" }) // можно с func
}

Выдаёт:

a
b
c

Но этот способ не работает при overloading, т.е. если есть несколько подходящих методов с параметром Func/FuncN.

В будущем возможен полный отказ от вызова func. Этот вызов нужен только из-за устаревшего определения “лямбды”.

Параметры без типа

Если анонимная функция определяется при помощи #-выражения непосредственно как аргумент параметра типа FuncN/Func без использования func при вызове метода, то можно не указывать и типы параметров этой анонимной функции:

def work(f: Func[Int, Int]) = {
  println(f.!(123))
}
def main = {    
  work: #(a){ a + 777 } // здесь a типа Int
}

Выдаёт:

900

Компилятор сам выводит типы из определения соответствующего FuncN/Func параметра.

{...}, ${...}, #{...}

Конструкции, которые выглядят похожими. Но смысл разный:

  • {...} - исполнение кода в скобках {}, результатом является результат последнего выражения (или пустота (), выражений нет)
  • ${...} - исполнение кода в скобках {}, но результатом является текущий контекст $.
  • #{...} - часть определения анонимной функции без параметров, тело функции задаётся в скобках {}

Внутри скобок {} всех трёх конструкций допускается несколько выражений, разделенных переносом строк (или ;), в том числе определения переменных fix/var.

Ограничение по числу параметров

В настоящее время Func имеет ограничение в 10 параметров анонимной функции (Func10):

trait Func10[P0, P1, P2, P3, P4, P5, P6, P7, P8, P9, R] {
  def `!`(p0: P0, p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9): R
}

Но в будущем (по мере необходимости) число параметров может быть увеличено.

Другие (произвольные) экземпляры Func/FuncN

Поскольку базой анонимной функции является трейты Func/FuncN, то есть и другие способы создания экземпляров, которые тоже будут считаться анонимными функциями.

Например, Map является функцией (но в тип результата добавляется пустота, обозначающая отсутствие ключа):

def work(f: Func[Int, String?]) = {
  1..3 as v.${
    println(s<<  #{v}: #{f.!(v).*onEmpty("?")}>>)
  }
}

def main = {   
  println("Function") 
  work: #(v) {
    if (v mod 2 == 0) "even" else "odd"
  } // передаём обычную анонимную функцию
  println("Map")
  fix m = map[Int, String]()
  m.!(1) = "one"
  work: m // передаём Map с неявным преобразованием трейта
}

Выдаёт:

Function
  1: odd
  2: even
  3: odd
Map
  1: one
  2: ?
  3: ?

И даже структура с полем ! может служить функцией (как и ручное определение метода !):

def work(f: Func[String]) = {
  println(" " + f.!)
}

struct Test(`!`: String)

def main = {   
  println("Function") 
  work: # { 
    "func"
  } // передаём обычную анонимную функцию
  println("Struct")
  fix t = Test("test")
  work: t // передаём структуру с неявным преобразованием в трейт
}

Выдаёт:

Function
 func
Struct
 test

Всё, что попадает под сигнатуру метода ! трейтов Func/FuncN, можно рассматривать как анонимные функции, пусть и со специфической реализацией.