Являются ли полиморфные функции "ограничительными" в Scala?
В книге "Функциональное программирование в Scala MEAP v10" автор упоминает
Полиморфные функции часто настолько ограничены своим типом, что имеют только одну реализацию!
и приводит пример
def partial1[A,B,C](a: A, f: (A,B) => C): B => C = (b: B) => f(a, b)
Что он подразумевает под этим утверждением? Являются ли полиморфные функции ограничительными?
3 ответа
Вот более простой пример:
def mysteryMethod[A, B](somePair: (A, B)): B = ???
Что делает этот метод? Оказывается, что этот метод может сделать только одно! Вам не нужно имя метода, вам не нужна реализация метода, вам не нужна документация. Тип сообщает вам все, что он мог бы сделать, и оказывается, что "все" в данном случае - это одно.
Итак, что это делает? Требуется пара (A, B)
и возвращает некоторое значение типа B
, Какое значение это возвращает? Может ли он построить значение типа B
? Нет, не может, потому что не знает, что B
является! Может ли он вернуть случайное значение типа B
? Нет, поскольку случайность является побочным эффектом и, следовательно, должна присутствовать в сигнатуре типа. Может ли он выйти во вселенную и принести немного B
? Нет, потому что это будет побочным эффектом и должен появиться в сигнатуре типа!
Фактически, единственное, что он может сделать, это вернуть значение типа B
это было передано в него, второй элемент пары. Итак, это mysteryMethod
действительно second
Метод, и его единственно разумная реализация:
def second[A, B](somePair: (A, B)): B = somePair._2
Обратите внимание, что в действительности, поскольку Scala не является ни чистым, ни тотальным, на самом деле метод может сделать еще несколько вещей: выбросить исключение (т.е. вернуть ненормально), войти в бесконечный цикл (т.е. вообще не возвращать), использовать отражение, чтобы выяснить фактический тип B
и рефлексивно вызывать конструктор для изготовления нового значения и т. д.
Однако, принимая во внимание чистоту (возвращаемое значение может зависеть только от аргументов), совокупность (метод должен возвращать значение в обычном порядке) и параметричность (в действительности он ничего не знает о A
а также B
), то есть на самом деле очень много, что вы можете сказать о методе, только взглянув на его тип.
Вот еще один пример:
def mysteryMethod(someBoolean: Boolean): Boolean = ???
Что это может сделать? Это всегда может вернуться false
и игнорировать его аргумент. Но тогда это будет чрезмерно ограничено: если он всегда игнорирует свой аргумент, то ему все равно, что это Boolean
и его тип скорее будет
def alwaysFalse[A](something: A): Boolean = false // same for true, obviously
Он всегда может просто вернуть свой аргумент, но, опять же, тогда он на самом деле не будет заботиться о логических значениях, и его тип будет лучше
def identity[A](something: A): A = something
Так что, на самом деле, единственное, что он может сделать, - это вернуть логическое значение, отличное от того, которое было передано, и, поскольку существует только два логических значения, мы знаем, что наш mysteryMethod фактически not
:
def not(someBoolean: Boolean): Boolean = if (someBoolean) false else true
Итак, здесь у нас есть пример, где типы не дают нам реализацию, но, по крайней мере, они дают (небольшой) набор из 4 возможных реализаций, только одна из которых имеет смысл.
(Кстати: оказывается, что существует только одна возможная реализация метода, который принимает A
и возвращает A
и это метод идентификации, показанный выше.)
Итак, резюмируем:
- чистота означает, что вы можете использовать только те строительные блоки, которые были переданы вам (аргументы)
- Сильная, строгая, статическая система типов означает, что вы можете использовать эти строительные блоки только таким образом, чтобы их типы совпадали
- тотальность означает, что вы не можете делать глупости (например, бесконечные циклы или исключения)
- параметричность означает, что вы вообще не можете делать никаких предположений о переменных типа
Думайте о своих аргументах как о частях машины, а о своих типах - как о соединителях этих частей машины. У вас будет только ограниченное количество способов соединить эти детали машины так, чтобы вы только соединяли совместимые разъемы, и у вас не осталось никаких оставшихся частей. Довольно часто, будет только один путь, или если есть несколько путей, то часто один, очевидно, будет правильным.
Это означает, что после того, как вы спроектировали типы своих объектов и методов, вам даже не нужно будет думать о том, как реализовать эти методы, потому что типы уже будут диктовать единственный возможный способ их реализации! Учитывая, сколько вопросов по Stackru в основном "как мне это реализовать?", Можете ли вы представить, как при его освобождении вообще не нужно думать об этом, потому что типы уже диктуют одну (или одну из нескольких) возможных реализаций?
Теперь посмотрите на сигнатуру метода в вашем вопросе и попробуйте поиграть с разными способами a
а также f
таким образом, что типы выстраиваются в линию, и вы используете оба a
а также f
и вы действительно увидите, что есть только один способ сделать это. (Как показали Крис и Пол.)
def partial1[A,B,C](a: A, f: (A,B) => C): B => C = (b: B) => f(a, b)
Вот, partial1
принимает в качестве параметров значение типа A и функцию, которая принимает параметр типа A и параметр типа B, возвращая значение типа C.
partial1
должен вернуть функцию, принимающую значение типа B и возвращающую C. Поскольку A, B и C являются произвольными, мы не можем применять какие-либо функции к их значениям. Таким образом, единственная возможность состоит в том, чтобы применить функцию f
к стоимости a
перешел к partial
и значение типа B, которое является параметром для функции, которую мы возвращаем.
Таким образом, вы в конечном итоге с единственной возможностью, которая находится в определении f(a,b)
Чтобы взять более простой пример, рассмотрим тип Option[A] => Boolean
, Есть только несколько способов реализовать это:
def foo1(x: Option[A]): Boolean = x match { case Some(_) => true
case None => false }
def foo2(x: Option[A]): Boolean = !foo1(x)
def foo3(x: Option[A]): Boolean = true
def foo4(x: Option[A]): Boolean = false
Первые два варианта в значительной степени одинаковы, а последние два тривиальны, поэтому, по сути, есть только одна полезная вещь, которую может сделать эта функция, - это сказать вам, Option
является Some
или же None
,
Пространство возможной реализации "ограничено" абстрактностью типа функции. поскольку A
является неограниченным, значение параметра может быть любым, поэтому функция никак не может зависеть от этого значения, потому что вы ничего не знаете о его значении. Единственное "понимание" функции, которое может иметь функция, - это структура Option[_]
,
Теперь вернемся к вашему примеру. Вы понятия не имеете, что C
Таким образом, вы не можете построить его самостоятельно. Поэтому созданная вами функция должна вызываться f
чтобы получить C
, И для того, чтобы позвонить f
необходимо указать аргументы типов A
а также B
, Опять же, поскольку нет никакого способа создать A
или B
Вы можете использовать только те аргументы, которые вам даны. Так что нет другой возможной функции, которую вы могли бы написать.