Композиция функций Scala totalFn (partialFn (totalFn (x)))

Я пытался составить три функции, и только средняя из них была PartialFunction. Я ожидаю, что результирующий тип также будет PartialFunction.

Пример:

val mod10: Int => Int = _ % 10
val inverse: PartialFunction[Int, Double] = { case n if n != 0 => 1.0 / n }
val triple: Double => Double = _ * 3

val calc: Int => Double = mod10 andThen inverse andThen triple

Однако calc не определен во всем своем домене. Он выдаст MatchError для каждого числа, делящегося на 10.

В чем причина возврата полной функции, когда хотя бы одна из функций в композиции является частичной?

Другой пример, когда композиция частичных функций приводит к другой частичной функции с неверными условиями предметной области:

val inverse: PartialFunction[Double, Double] = { case n if n != 0 => 1.0 / n }
val arcSin: PartialFunction[Double, Double] = { 
   case n if math.abs(n) <= 1 => math.asin(n)
}

val calc: PartialFunction[Double, Double] = inverse andThen arcSin

Я ожидаю, что домен calc будет (-Infinity, -1] union [1, Infinity), но вызов calc.lift(0.5) вызовет MathError вместо возврата None, потому что ввод находится в домене первой функции.

Спасибо, Норберт.


person norbertk    schedule 19.03.2019    source источник


Ответы (3)


Пример 1. В чем причина возврата общей функции, если хотя бы одна из функций в композиции является частичной?

Это потому, что первая функция в вашем первом примере является общей функцией (Function1) и ее andThen возвращает функцию Function1 независимо от того, является ли вторая функция полной или частичной:

def andThen[A](g: (R) => A): (T1) => A

Я предполагаю, что команда разработчиков языка Scala предпочитает более обобщенное возвращаемое значение, поскольку PartialFunction является подклассом Function и позволяет пользователям создавать специализированный код по мере необходимости.

Пример 2: вызов calc.lift(0.5) вызовет MathError вместо возврата None

Из PartialFunction API doc, объединение двух частичных функций с помощью andThen вернет частичную функцию с тем же доменом, что и первая частичная функция:

 def andThen[C](k: (B) => C): PartialFunction[A, C]

Таким образом, результирующая составная функция игнорирует тот факт, что inverse(0.5) (т. е. 2.0) находится за пределами домена второй частичной функции arcSin.


Итак, при составлении функции (полной или частичной) с частичной функцией с использованием andThen как мы можем заставить ее возвращать частичную функцию с правильным доменом?

Подобно тому, что продемонстрировано в этом SO Q&A, можно улучшить andThen с помощью пары неявных классов, чтобы ограничить домен результирующей составленной функции в подмножество домена первой функции, которые возвращают значения в пределах домена частичной функции:

object ComposeFcnOps {
  implicit class TotalCompose[A, B](f: Function[A, B]) {
    def andThenPartial[C](that: PartialFunction[B, C]): PartialFunction[A, C] =
      Function.unlift(x => Option(f(x)).flatMap(that.lift))
  }

  implicit class PartialCompose[A, B](pf: PartialFunction[A, B]) {
    def andThenPartial[C](that: PartialFunction[B, C]): PartialFunction[A, C] =
      Function.unlift(x => pf.lift(x).flatMap(that.lift))
  }
}

Тестирование с примерами функций:

import ComposeFcnOps._

val mod10: Int => Int = _ % 10
val inverse1: PartialFunction[Int, Double] = { case n if n != 0 => 1.0 / n }
val triple: Double => Double = _ * 3

val calc1 = mod10 andThenPartial inverse1 andThen triple
// calc1: PartialFunction[Int,Double] = <function1>

calc1.isDefinedAt(0)
// res1: Boolean = false

val inverse2: PartialFunction[Double, Double] = { case n if n != 0 => 1.0 / n }
val arcSin: PartialFunction[Double, Double] = { 
   case n if math.abs(n) <= 1 => math.asin(n)
}

val calc2 = inverse2 andThenPartial arcSin
// calc2: PartialFunction[Double,Double] = <function1>

calc2.isDefinedAt(0.5)
// res2: Boolean = false

calc2.lift(0.5)
// res3: Option[Double] = None
person Leo C    schedule 19.03.2019
comment
Спасибо Лео! Это было бы отличным расширением языка :) Я обязательно начну его использовать. - person norbertk; 20.03.2019
comment
К вашему сведению @norbertk, я расширил ответ, чтобы также охватить случай составления общей функции с частичной функцией с использованием andThen. - person Leo C; 21.03.2019
comment
Прочитав ваш первый пост, я создал класс, в который вошли как andThen, так и compose. Я просто не думал о создании неявных классов для решения этой проблемы, но это хороший способ добавить недостающую функциональность. ???? - person norbertk; 21.03.2019

Я думаю, что ошибка - это ваше единственное ожидаемое ненулевое значение.

{ case n if n != 0 => 1.0 / n } 

тогда что, если он будет равен нулю, то это причина ошибки совпадения.

{ 
   case n if n != 0 => 1.0 / n   // non-zero value.
   case n if n == 0 =>           // zero value.

} 

Надеюсь, поможет.

person Rex    schedule 19.03.2019
comment
Спасибо Рекс за ответ. Я знаю, откуда берется MatchError. Проблема не в том, что он выбрасывается, а в том, что тип результата не является PartialFunction, т.е. я не могу использовать .lift для преобразования результата в Option[T]. Значимого значения по умолчанию нет, и конечная функция calc должна выдавать ошибку MatchError. - person norbertk; 19.03.2019
comment
def calc(x: Int)(a: PartialFunction[Int, Double])(c: Double) = mod10, затем обратное и затем тройное - person Rex; 19.03.2019
comment
Можно ли использовать что-то подобное? - person Rex; 19.03.2019
comment
Я не думаю, что этот код означает то, что вы хотите, чтобы он означал. Что делают вторые два параметра (a и c)? - person norbertk; 19.03.2019

andThen определяется на Function1 и просто не предназначен для составления частичных функций. Поэтому я рекомендую поднимать их до тотальных функций перед использованием.

val calc = Function.unlift(mod10 andThen inverse.lift andThen (_.map(triple)))

И

val calc = Function.unlift(inverse.lift andThen (_.flatMap(arcSin.lift)))
person Brian McCutchon    schedule 19.03.2019