Являются ли полиморфные функции "ограничительными" в Scala?

В книге "Функциональное программирование" в Scala MEAP v10 автор упоминает

Полиморфные функции часто настолько ограничены их типом, что у них есть только одна реализация!

и дает пример

def partial1[A,B,C](a: A, f: (A,B) => C): B => C = (b: B) => f(a, b)

Что он подразумевает под этим утверждением? Ограничены ли полиморфные функции?

Ответ 1

Вот более простой пример:

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, и это метод идентификации, показанный выше.)

Итак, чтобы повторить:

  • чистота означает, что вы можете использовать только строительные блоки, которые были переданы вам (аргументы)
  • сильная, строгая система статического типа означает, что вы можете использовать только эти строительные блоки таким образом, чтобы их типы выстраивались в линию
  • тотальность означает, что вы не можете делать глупые вещи (например, бесконечные циклы или исключения для исключения)
  • Параметричность означает, что вы вообще не можете делать какие-либо предположения о своих переменных типа

Подумайте о своих аргументах как части машины и ваших типах в качестве разъемов на этих частях машины. Будет ограниченное количество способов соединения этих частей машины таким образом, чтобы вы только подключали совместимые разъемы, и у вас нет оставшихся частей. Достаточно часто, будет только один способ, или если есть несколько способов, тогда часто один будет, очевидно, правильным.

Это означает, что после того, как вы разработали типы своих объектов и методов, вам даже не придется думать о том, как реализовать эти методы, потому что типы уже будут диктовать единственный возможный способ их реализации! Учитывая, сколько вопросов в StackOverflow в основном "как это реализовать?", Можете ли вы представить себе, как освободить его от необходимости вообще не думать об этом, потому что типы уже диктуют одну (или одну из нескольких) возможных реализаций

Теперь посмотрите на подпись метода в своем вопросе и попробуйте по-разному объединить A и f таким образом, чтобы типы выстраивались в линию, и вы используете как A, так и f и вы действительно увидите, что есть только один способ сделать это. (Как показали Крис и Пол.)

Ответ 2

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)

Ответ 3

Чтобы взять более простой пример, рассмотрим тип 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 самостоятельно, единственное, что вы можете сделать, это использовать аргументы, которые вам даны. Поэтому нет другой возможной функции, которую вы могли бы написать.