Kotlin. Анонімні функції (лямбда). Особливості використання

Анонімні функції (лямбда). Особливості використання. Приклади


Зміст


1. Неявне повернення у лямбда-виразах

В анонімній функції (лямбда-виразі) використання ключового слова return для повернення даних є необов’язковим (на відміну від іменованих функцій). Автоматично (неявно) повертається результат останньої інструкції. Наприклад, якщо останньою інструкцією був рядок, то результатом повернення лямбда-виразу (анонімної функції) буде тип String.

Такий синтаксис є необхідністю, оскільки застосування ключового слова return викличе неоднозначність. Незрозуміло, чи повертати результат з функції, яка викликала лямбда-вираз, чи повертати результат з лямбда-виразу.

Приклад.

fun main(args: Array<String>) {
  // 1. Неявно повертається тип String
  // 1.1. Оголосити анонімну функцію
  val FuncHello: () -> String = {
    "Hello, world!"
  }

  // 1.2. Вивести результат виклику анонімної функції
  val res1 = FuncHello()
  println(res1) // Hello, world!

  // 2. Неявно повертається тип Int
  // 2.1. Оголосити анонімну функцію, яка приймає один параметр типу Int
  //      і повертає тип Int неявно
  val Mult2: (Int) -> Int = { a ->
    a * 2 // неявне повернення типу Int, слово return відсутнє
  }

  // 2.2. Викликати функцію Mult2 для того, щоб помножити число на 2
  val res2:Int
  res2 = Mult2(100)
  println(res2) // 200
}

Результат

Hello, world!
200

 

2. Передача аргументів в анонімну функцію

Анонімна функція може приймати будь-яку кількість аргументів. Для того, щоб анонімна функція приймала аргументи потрібно описати типи цих аргументів. У найбільш загальному випадку вигляд анонімної функції, яка приймає N аргументів виглядає приблизно так

val FuncName: (paramType1, paramType2, ..., paramTypeN) -> returnType {
  param1, param2, ..., paramN -> expression
}

тут

  • paramType1, paramType2, paramTypeN – типи параметрів, які отримує функція;
  • param1, param2, paramN – імена параметрів, які отримує функція. Параметр param1 має тип paramType1, параметр param2 має тип paramType2 і так далі;
  • expression – один або декілька виразів.

Приклад.

fun main(args: Array<String>) {
  // 1. Анонімна функція, яка отримує радіус кола і повертає площу круга
  // 1.1. Оголошення функції
  val Area: (Double) -> Double = { r ->
    Math.PI * r * r
  }

  // 1.2. Виклик функції
  println("Area(2.0) = " + Area(2.0))

  // 2. Анонімна функція, яка отримує 3 параметри типу Float
  //   і повертає їх середнє арифметичне
  // 2.1. Оголошення функції
  val Avg3: (Float, Float, Float) -> Float = { a, b, c ->
    (a + b + c) / 3
  }

  // 2.2. Виклик функції
  println("Avg(7, 2, 8) = " + Avg3(7F, 2F, 8F))
}

Результат

Area(2.0) = 12.566370614359172
Avg(7, 2, 8) = 5.6666665

 

3. Заміна аргументу. Ключове слово it

Якщо анонімна функція приймає тільки один аргумент, то ім‘я цього аргументу в тілі функції може бути замінене ключовим словом it. Таким чином є два альтернативних випадки звернення до аргументу:

  • безпосередньо за іменем аргументу;
  • з використанням ключового слова it.

Ключове слово it в тілі анонімної функції реалізовано з специфікатором val. А це означає, що використовувати ключове слово it в лівій частині оператора присвоювання заборонено

it = value // помилка, заборонено

Важливо зауважити, що ключове слово it у тілі анонімної функції описує дані недостатньо зрозуміло. Якщо анонімна функція має складніший код, то краще використовувати безпосередньо імена аргументів. Ключове слово it доцільно використовувати у випадку коротких лямбда-виразів.

Приклад.

fun main(args: Array<String>) {
  // 1. Анонімна функція, яка отримує один аргумент - радіус кола
  //    і повертає довжину кола
  // 1.1. Оголошення функції без використання ключового слова it
  val Circumference1: (Double)->Double = { r ->
    2 * Math.PI * r // аргумент має ім'я r
  }

  // 1.2. Оголошення функції з використанням ключового слова it,
  //      тут ім'я аргументу r не вказується
  val Circumference2: (Double) -> Double = {
    2 * Math.PI * it // використовується it
  }

  // 1.3. Виклик функцій
  println("Circumference1(5.0) = " + Circumference1(5.0))
  println("Circumference2(5.0) = " + Circumference2(5.0))

  // 2. Анонімна функція, яка отримує 1 параметр - число типу Int
  //    і повертає суму цифр цього числа
  // 2.1. Оголошення функції
  val SumNumbers: (Int) -> Int = {
    var sum = 0
    var t = it // використовується it замість імені аргументу
    while (t>0) {
      sum = sum + t%10
      t = t/10
    }
    sum
  }

  // 2.2. Виклик функції
  val summ = SumNumbers(982)

  // 2.3. Вивід результату
  println("summ = " + summ) // summ = 19
}

Результат

Circumference1(5.0) = 31.41592653589793
Circumference2(5.0) = 31.41592653589793
summ = 19

 

4. Оголошення анонімної функції без явно заданого типу

Анонімна функція може бути оголошена без задавання типу повернення. У цьому випадку тип визначається автоматично. Така особливість анонімних функцій робить код більш лаконічним.

У найбільш загальному випадку оголошення анонімної функції з автоматичним визначенням типу має вигляд

val FuncName = { arg1:typeArg1, arg2:typeArg2, ..., argN:typeArgN ->
  // Інструкції
  // ...
}

тут

  • FuncName – ім’я функції;
  • arg1, arg2, argN – імена аргументів, які отримує функція;
  • typeArg1, typeArg2, typeArgN – відповідно типи аргументів arg1, arg2, argN.

Якщо анонімна функція не отримує параметрів, то вигляд функції приблизно наступний

val FuncName = {
  // Інструкції
  // ...
}

Приклад.

fun main(args: Array<String>) {
  // 1. Анонімна функція, яка отримує 3 числа типу Int
  //    і знаходить максимальне між ними
  // 1.1. Оголошення функції, тип повернення визначається автоматично
  val Max3 = { num1: Int, num2: Int, num3: Int ->
    var max = num1
    if (max < num2) max = num2
    if (max < num3) max = num3
    max
  }

  // 1.2. Виклик функції
  val res = Max3(5, 9, 7)
  println("Max3(5, 9, 7) = " + res)

  // ------------------------------------------------
  // 2. Анонімна функція, яка отримує рядок і символ і повертає
  //    кількість входжень символу в рядку
  // 2.1. Оголосити функцію
  val GetCharOccurences = { s: String, c: Char ->
    var count: Int = 0
    for (t in s)
      if (t == c)
        count++
    count
  }

  // 2.2. Виклик функції
  val nOccur = GetCharOccurences("abc abd def", 'a')
  println("nOccur = " + nOccur)

  // ----------------------------------------------------
  // 3. Анонімна функція, яка не отримує параметрів
  // 3.1. Оголошення
  val PrintHello = {
    println("Hello, world!")
  }

  // 3.2. Виклик
  PrintHello()
}

Результат

Max3(5, 9, 7) = 9
nOccur = 2
Hello, world!

 

5. Передача лямбди (анонімної функції) в якості параметру іншої функції

У будь-яку функцію можна передавати будь-яку кількість аргументів будь-якого типу. Крім даних різних типів, цими аргументами можуть бути лямбди або анонімні функції.

Процес передачі лямбди в іншу функцію може виглядати так. У тілі деякої функції Fun1():

  • оголошується анонімна функція (лямбда), яка передається в іншу функцію Fun2();
  • реалізується виклик функції Fun2() з передачею їй оголошеної лямбди.

Вигляд функції Fun1() приблизно такий

fun Fun1(...) {

  ...

  // Оголошення анонімної функції (лямбди)
  val Lambda = { params_Lambda ->
    // Інструкції в тілі лямбди
    ...
  }

  ...

  // Виклик функції Fun2() з передачею лямбди як аргументу
  Fun2(args, Lambda); // передається ім'я лямбди
}

тут

  • Fun1 – ім’я деякої функції в якій оголошується лямбда;
  • args – деякі додаткові аргументи, які передаються у функцію Fun2();
  • Lambda – ім’я анонімної функції;
  • params_Lambda – параметри, які отримує лямбда.

У свою чергу, функція Fun2():

  • приймає в якості вхідного параметру специфікацію лямбди без реалізації;
  • використовує виклик лямбди у своєму коді.

Приблизний вигляд функції Fun2() наступний:

fun Fun2(parameters, Lambda(params_Lambda)->returnLambda)
{
  ...

  // Використання лямбди в тілі функції
  Lambda(args_Lambda)

  ...
}

тут

  • parameters – додаткові параметри, які отримує функція Fun2() крім лямбди;
  • params_Lambda – список параметрів лямбди, розділених комою;
  • return_Lambda – тип даних, що повертає лямбда.

Приклад.

У прикладі формується екземпляр лямбди D(), який на основі коефіцієнтів a, b, c обчислює дискримінант. Потім цей лямбда передається у функцію Equation(), яка викликає його код.

fun main(args: Array<String>) {
  // Передача в анонімну функцію іншої функції.
  // Розв'язок квадратного рівняння
  // 1. Оголосити анонімну функцію, яка знаходить дискримінант
  val D = { a: Double, b: Double, c: Double ->
    b * b - 4 * a * c
  }

  // 2. Викликати функцію Equation(), передається ім'я лямбди D
  Equation(3.0, -3.0, -2.0, D)
}

// Функція, яка знаходить розв'язок квадратного рівняння і виводить його на екран,
// функція отримує параметром анонімну функцію, яка обчислює дискримінант
fun Equation(a:Double, b:Double, c:Double, D:(Double, Double, Double)->Double) {
  if (D(a,b,c)<0)
    println("Solution has no roots.")
  else {
    val x1: Double = (-b - Math.sqrt(D(a, b, c))) / (2 * a) // виклик лямбди D(a,b,c)
    val x2: Double = (-b + Math.sqrt(D(a, b, c))) / (2 * a) // виклик лямбди D(a,b,c)
    println("x1 = " + x1)
    println("x2 = " + x2)
  }
}

Результат

x1 = -0.4574271077563381
x2 = 1.457427107756338

 

6. Передача лямбди у функцію за скороченим синтаксисом

Лямбда може передаватись у функцію за спрощеним синтаксисом без вказання круглих дужок (). У такому випадку, способи використання лямбди розширюються.

Використання лямбди з круглими дужками виглядає приблизно так

Fn({ instructions })

тут

  • Fn – ім’я функції, в яку передається лямбда;
  • instructions – деякі операції, що використовуються у лямбді.

Використання лямбди без круглих дужок виглядає простіше

Fn { instructions }

Скорочений синтаксис можна використовувати у випадках, коли лямбда передається у функцію останнім аргументом.

Приклад.

fun main(args: Array<String>) {
  // Спрощений синтаксис передачі лямбди
  // Демонструється пошук числа 5 в масиві цілих чисел
  // 1. Створити масив з 10 цілих чисел
  var AI : Array<Int> = arrayOf(
    7, 3, 7, 9, 5, 5, 6, 4, 7, 2
  )

  // 2. Обчислити кількість чисел 5 з використанням дужок (),
  //   використати стандартну функцію count()
  val num5 = AI.count( { it == 5 } ) // використовуються дужки ()
  println("num5 = " + num5)

  // 3. Обчислити кількість чисел 7 з допомогою спрощеного запису
  val num7 = AI.count { it == 7 } // дужки () відсутні
  println("num7 = " + num7)
}

Результат

num5 = 2
num7 = 3

 

7. Вбудовування. Ключове слово inline

Використання анонімних функцій (лямбд) призводить до неефективних витрат пам’яті. Це пов’язано з тим, що на кожну лямбду створюється відповідний екземпляр об’єкту. Цей екземпляр містить дані (змінні) для яких виділяється пам’ять. При передачі лямбди у функцію як параметра з цих даних створюється ще одна копія в пам’яті, тобто, створюється ще один екземпляр об’єкту.

Щоб уникнути створення копії лямбди, у мові Kotlin передбачено використання так званого вбудовування (інлайнінгу). Вбудовування забезпечується з застосуванням ключового слова inline перед оголошенням функції. Приблизний вигляд такого оголошення наступний

inline fun Func(parameters, Lambda(params_Lambda)->return_Lambda) {
  ...
}

Вбудовування у програмах може застосовуватись у будь-яких функціях за винятком рекурсивних функцій. Рекурсивна функція – це функція, яка в своєму коді викликає сама себе.

Приклад.

Демонструється програма обчислення площі трикутника за заданими сторонами. У програмі оголошується лямбда, яка обчислює півпериметр. Потім ця лямбда передається у функцію AreaTriangle(), яка обчислює площу трикутника за трьома сторонами. Код функції AreaTriangle() оптимізується завдяки використанню ключового слова inline.

fun main(args: Array<String>) {
  // Вбудовування. Ключове слово inline
  // Обчислення площі трикутника.
  // 1. Оголосити лямбду, яка обчислює півпериметр за трьома сторонами
  val P: (Double, Double, Double) -> Double = { a, b, c ->
    (a + b + c) / 2
  }

  // 2. Викликати обчислення площі для трикутника зі сторонами 5, 6, 7
  val area = AreaTriangle(5.0,6.0,7.0, P)

  // 3. Вивести результат
  print("AreaTriangle(5,6,7) = " + area)
}

// inline-функція, яка обчислює площу за формулою Герона
inline fun AreaTriangle(a:Double, b:Double, c:Double, P:(Double,Double,Double)->Double) : Double {
  // 1. Перевірка, чи з сторін a, b, c можна утворити трикутник
  if (((a+b)<c)||((b+c)<a)||((a+c)<b))
    return 0.0

  // 2. Обчислити площу, при обчисленні використовується анонімна функція P()
  val area = Math.sqrt(P(a,b,c)*(P(a,b,c)-a)*(P(a,b,c)-b)*(P(a,b,c)-c))

  // 3. Повернути результат
  return area
}

Результат

AreaTriangle(5,6,7) = 14.696938456699069

 

8. Посилання на функцію. Передача посилання на функцію в іншу функцію. Оператор ::

Якщо оголошується функція, то можна сформувати посилання на цю функцію. Посилання на функцію оголошується з допомогою оператора :: за наступним синтаксисом

::FuncName

тут

  • FuncName – ім’я функції, яка була раніше оголошена з використанням ключового слова fun або це є анонімна функція (лямбда).

Посилання на функцію можна використовувати будь-де, де допускається лямбда-вираз. З допомогою посилань на функцію можна організовувати свого роду диспетчеризацію виконаної роботи у програмі.

Приклад.

У прикладі оголошуються 3 функції Length(), Area(), Volume(), які обчислюють відповідно довжину кола, площу круга та об’єм кулі. Усі 3 функції отримують 1 параметр – радіус.

У функцію Calc() передається посилання на функцію, яка отримує параметр типу Double. З функції Calc() відбувається виклик іншої функції, яка була передана йому як параметр.

З функції main() вдбувається виклик функції Calc(). У функцію Calc() по черзі передаються посилання на функції Length(), Area(), Volume() для отримання результату.

fun main(args: Array<String>) {
  // Посилання на функцію

  // 1. Отримати радіус
  var radius : Double
  print("radius = ")
  radius = readLine().toString().toDouble()

  // 2. Обчислити довжину кола з допомогою функції Calc() та вивести її,
  //   у функцію передається функція Length() за синтаксисом ::Length
  Calc("Length(" + radius + ") = ", radius, ::Length)

  // 3. Обчислити площу круга та вивести її,
  //   у функцію Calc() передається функція Length()
  Calc("Area(" + radius + ") = ", radius, ::Area)

  // 4. Обчислити об'єм кулі та вивести його функцією Calc()
  val v = ::Volume // ::Volume - отримати посилання на функцію
  Calc("Volume(" + radius + ") = ", radius, v) // передається ::Volume
}

// Функція, яка отримує посилання на іншу функцію в якості параметру,
// функція викликає вхідну функцію
fun Calc(message:String, radius:Double, Func: (Double) -> Double) {
  // Вивести на екран результат виклику функції Fun()
  println(message + Func(radius))
}

// Функція, яка за заданим радіусом обчислює довжину кола
fun Length(radius:Double) =
    2.0*Math.PI*radius

// Функція, яка за заданим радіусом обчислює площу круга
fun Area(radius:Double) =
    Math.PI*radius*radius

// Функція, яка обчислює об'єм кулі
fun Volume(radius:Double) =
    4.0/3.0*Math.PI*radius*radius*radius

Результат

radius = 3
Length(3.0) = 18.84955592153876
Area(3.0) = 28.274333882308138
Volume(3.0) = 113.09733552923255

 

9. Повернення функції з іншої функції

Функція може повертати іншу функцію. Повернення функції відбувається оператором return. Повертатись може як анонімна функція так і посилання на іменовану функцію.

У випадку повернення анонімної функції код повернення приблизно такий

fun Func(parameters) {

  ...

  return {
    // Інструкції в тілі анонімної функції
    // ...
  }
}

тут

  • Func – ім’я функції, з якої повертається анонімна функція;
  • parameters – параметри функції.

Якщо повертається іменована функція, що реалізована в програмі з використанням ключового слова fun, то код повернення приблизно наступний

fun Func(parameters) {
  ...
  return ::NamedFunc // повернути іменовану функцію
}

fun NamedFunc(parameters2) {
  ...
}

Приклад 1.

У прикладі демонструється повернення функцією Calc() однієї з трьох анонімних функцій, які обчислюють довжину кола, площу круга та об’єм кулі. У функції main() викликається функція Calc() для різних випадків.

fun main(args: Array<String>) {

  // Викликати функцію Calc
  var fn1 = Calc(1) // отримати функцію обчислення довжини кола
  val length = fn1(2.0) // обчислити довжину кола радіусу 2.0
  println("length = " + length)

  var fn2 = Calc(2) // отримати функцію обчислення площі круга
  val area = fn2(2.0)     // обчислити площу круга радіусу 2.0
  println("area = " + area)

  // Обчислити об'єм кулі радіусу 3.0
  val volume = Calc(3)(3.0)
  println("volume = " + volume)
}

// Функція, яка отримує ціле число і на основі цього числа повертає відповідну функцію
fun Calc(numFunc:Int): (Double)->Double {

  // Повернути функцію обчислення довжини кола
  if (numFunc==1)
    return { radius ->
      2*Math.PI*radius
    }

  // Повернути функцію обчислення площі круга
  if (numFunc==2)
    return { radius ->
      Math.PI*radius*radius
    }

  // Повернути функцію обчислення об'єму кулі
  return { radius ->
    4.0/3*Math.PI*radius*radius*radius
  }
}

Результат

length = 12.566370614359172
area = 12.566370614359172
volume = 113.09733552923255

Приклад 2.

Рішення попереднього прикладу іншим способом. Тут функція Calc() повертає посилання на іменовані функції Length(), Area(), Volume().

fun main(args: Array<String>) {

  // Викликати функцію Calc() для різних випадків
  var fn1 = Calc(1) // отримати функцію обчислення довжини кола
  val length = fn1(1.0) // обчислити довжину кола радіусу 1.0
  println("length = " + length)

  var fn2 = Calc(2) // отримати функцію обчислення площі круга
  val area = fn2(1.0)     // обчислити площу круга радіусу 1.0
  println("area = " + area)

  // Обчислити об'єм кулі радіусу 3.0
  val volume = Calc(3)(1.0)
  println("volume = " + volume)
}

// Функція, яка отримує ціле число і на основі цього числа повертає відповідну функцію
fun Calc(numFunc:Int): (Double)->Double {
  // Якщо параметр рівний 1,
  // то повернути посилання на функцію Length
  if (numFunc==1)
    return ::Length

  // Якщо параметр рівний 2,
  // то повернути посилання на функцію Area
  if (numFunc==2) return ::Area

  // Інакше повернути посилання на функцію Volume
  return ::Volume
}

// Функція, яка за заданим радіусом обчислює довжину кола
fun Length(radius:Double) =
    2.0*Math.PI*radius

// Функція, яка за заданим радіусом обчислює площу круга
fun Area(radius:Double) =
    Math.PI*radius*radius

// Функція, яка обчислює об'єм кулі
fun Volume(radius:Double) =
    4.0/3.0*Math.PI*radius*radius*radius

Результат

length = 6.283185307179586
area = 3.141592653589793
volume = 4.1887902047863905

 


Споріднені теми