Анонимные функции (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 работает по-разному.
В трейте допускается this (в expr1), что будет означать сам экземпляр трейта. Это позволяет использовать рекурсию:
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
А вот #-выражение не позволяет использовать this (в expr2), это вызывает ошибку компиляции.
Проблема в том, что будет конфликт: this может восприниматься и как this со значением самой анонимной функции, и как внешний this для замыкания:
def Int test = func: #{ println(this) } - здесь this какого типа?
Кроме того, использование this для обозначения самой анонимной функции (как в создании экземпляра трейта) требует знания типа результата этой анонимной функции (для типизации Func/FuncN), а он будет выведен только после компиляции тела анонимной функции, где этот this и используется.
Есть важное отличие и при использовании return.
В первом определении (через трейт) компилятор допускает использование return (в expr1), который делает возврат из определяемого метода `!`.
Во втором определении (через func и #) компилятор полностью запрещает использование return (в expr2). По аналогичной с 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, можно рассматривать как анонимные функции, пусть и со специфической реализацией.