Частичная функция – это любая функция, принимающая только один аргумент, которая определена (т. е. действительна) только для определенного диапазона значений ее аргументов. Например, Math.asin
определено только для значений аргументов в диапазоне [-1.0, 1.0]
и не определено для значений за пределами этого диапазона, поэтому это частичная функция. Например, если мы вызываем Math.asin(5.0)
, мы возвращаем NaN
, что означает, что функция не определена для этого аргумента.
Обратите внимание, что частичная функция не обязательно должна генерировать исключение; ему просто нужно сделать что-то кроме возврата действительного значения.
Ключевым принципом функционального программирования является прозрачность ссылок (RT), означающая, что мы должны иметь возможность заменить выражение (например, вызов функции ) со значением этого выражения без изменения смысла программы. (Чтобы узнать больше по этой теме, я настоятельно рекомендую вам прочитать Функциональное программирование в Scala Кьюзано и Бьярнасона.) Очевидно, что это не работает, если возникает исключение или возвращается недопустимое значение. Чтобы вызовы частичных функций были ссылочно прозрачными, мы можем вызывать их только со значениями аргументов, для которых они определены, или нам нужно элегантно обрабатывать неопределенные значения. Итак, как мы можем узнать, определена ли частичная функция для некоторого произвольного значения аргумента?
В Scala частичные функции можно выразить как подкласс scala.PartialFunction
, что позволяет нам ответить на этот вопрос.
Давайте посмотрим на ваш пример в сеансе Scala REPL...
$ scala
Welcome to Scala 2.12.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_171).
Type in expressions for evaluation. Or try :help.
scala> val second: List[Int] => Int = {
| case x :: y :: _ => y
| }
<console>:11: warning: match may not be exhaustive.
It would fail on the following inputs: List(_), Nil
val second: List[Int] => Int = {
^
second: List[Int] => Int = $$Lambda$3181/1894473818@27492c62
Так что мы только что сделали? Мы определили second
как ссылку на функцию, которая принимает аргумент List[Int]
и возвращает Int
(второе значение в списке).
Вы заметите, что компилятор Scala понимает, что это не будет соответствовать всем случаям, и предупреждает вас об этом. Это частичная функция, в том смысле, что она не работает для некоторых аргументов, но это не экземпляр scala.PartialFunction
, в чем мы можем убедиться следующим образом:
scala> second.isInstanceOf[PartialFunction[List[Int], Int]]
res0: Boolean = false
Между прочим, тип List[Int] => Int
является сокращением для scala.Function1[List[Int], Int]
, поэтому тип second
s является экземпляром этого типа:
scala> second.isInstanceOf[Function1[List[Int], Int]]
res1: Boolean = true
Вызов этой версии функции дает указанные вами результаты:
scala> second(List(1, 2, 3))
res1: Int = 2
scala> second(Nil)
scala.MatchError: List() (of class scala.collection.immutable.Nil$)
at .$anonfun$second$1(<console>:11)
at .$anonfun$second$1$adapted(<console>:11)
... 36 elided
Проблема в том, что если у нас просто есть какое-то значение списка, l
, и мы не знаем, что находится в этом списке, мы не знаем, получим ли мы исключение, если передадим его функции, на которую ссылается second
. Теперь мы могли бы поместить вызов в блок try
и catch
любое исключение, но это слишком многословно и не соответствует стилю функционального программирования. В идеале мы хотели бы знать, можем ли мы сначала вызвать функцию, чтобы избежать исключения. К сожалению, по экземпляру Function1
нельзя сказать:
scala> second.isDefinedAt(Nil)
<console>:13: error: value isDefinedAt is not a member of List[Int] => Int
second.isDefinedAt(Nil)
^
Что нам нужно, так это объявить second
, чтобы иметь тип PartialFunction[List[Int], Int]
следующим образом:
scala> val second: PartialFunction[List[Int], Int] = {
| case x :: y :: _ => y
| }
second: PartialFunction[List[Int],Int] = <function1>
(Кстати, обратите внимание, что у вас есть опечатка в вашем вопросе для этого кода - это должно быть определено выше.)
Теперь у нас нет никаких предупреждений! Мы сказали компилятору, что это экземпляр PartialFunction
, поэтому компилятор знает, что он не определен для некоторых аргументов, поэтому предупреждения излишни. Теперь мы можем проверить этот факт:
scala> second.isInstanceOf[PartialFunction[List[Int], Int]]
res6: Boolean = true
Теперь мы также можем проверить, определен ли он для определенных значений:
scala> second.isDefinedAt(Nil)
res7: Boolean = false
scala> second.isDefinedAt(List(1, 2))
res9: Boolean = true
и так далее. (Компилятор Scala, описанный в книге, способен реализовать за нас эту волшебную функцию isDefinedAt
.)
Итак, значит ли это, что теперь мы должны написать такой код:
def getSecondValue(l: List[Int]): Option[Int] = {
// Check if second is defined for this argument. If so, call it and wrap in Some.
if(second.isDefinedAt(l)) Some(second(l))
// Otherwise, we do not have a second value.
else None
}
Ну, это тоже немного многословно. К счастью, когда second
является экземпляром PartialFunction
, мы можем переписать приведенное выше как:
def getSecondValue(l: List[Int]): Option[Int] = second.lift(l)
Метод lift
превращает частичную функцию в полную функцию, которая возвращает определенное значение для каждого аргумента: если аргумент second
определен, мы получаем Some(value)
; иначе получаем None
.
Вы обнаружите, что концепция частичных функций и PartialFunction
становится более полезной по мере того, как вы лучше знакомитесь с функциональным программированием. Если вы не понимаете прямо сейчас, не волнуйтесь; все станет ясно.
person
Mike Allen
schedule
17.05.2018