Я смотрел на F #, занимался низкоуровневыми уроками, поэтому мои знания очень ограничены. Тем не менее, для меня было очевидно, что его стиль был по сути функциональным, а ОО был скорее дополнением - гораздо больше модульной системы ADT +, чем настоящий ОО. Чувство, которое я испытываю, лучше всего описать так, как если бы все методы были статическими (как в Java static).
См., Например, любой код, использующий оператор канала (|>
). Возьмите этот фрагмент из записи в Википедии на F # :
[1 .. 10]
|> List.map fib
(* equivalent without the pipe operator *)
List.map fib [1 .. 10]
Функция map
не является методом экземпляра списка. Вместо этого он работает как статический метод в модуле List
, который принимает экземпляр списка в качестве одного из своих параметров.
Скала, с другой стороны, полностью ОО. Давайте начнем, во-первых, с Scala-эквивалента этого кода:
List(1 to 10) map fib
// Without operator notation or implicits:
List.apply(Predef.intWrapper(1).to(10)).map(fib)
Здесь map
- это метод для экземпляра List
. Статические методы, такие как intWrapper
на Predef
или apply
на List
, встречаются гораздо реже. Тогда есть функции, такие как fib
выше. Здесь fib
не является методом на int
, но и не является статическим методом. Вместо этого это объект - второе основное отличие, которое я вижу между F # и Scala.
Давайте рассмотрим реализацию F # из Википедии и эквивалентную реализацию Scala:
// F#, from the wiki
let rec fib n =
match n with
| 0 | 1 -> n
| _ -> fib (n - 1) + fib (n - 2)
// Scala equivalent
def fib(n: Int): Int = n match {
case 0 | 1 => n
case _ => fib(n - 1) + fib(n - 2)
}
Приведенная выше реализация Scala является методом, но Scala преобразует его в функцию, чтобы иметь возможность передать ее в map
. Я модифицирую его ниже, чтобы он стал методом, который вместо этого возвращает функцию, чтобы показать, как функции работают в Scala.
// F#, returning a lambda, as suggested in the comments
let rec fib = function
| 0 | 1 as n -> n
| n -> fib (n - 1) + fib (n - 2)
// Scala method returning a function
def fib: Int => Int = {
case n @ (0 | 1) => n
case n => fib(n - 1) + fib(n - 2)
}
// Same thing without syntactic sugar:
def fib = new Function1[Int, Int] {
def apply(param0: Int): Int = param0 match {
case n @ (0 | 1) => n
case n => fib.apply(n - 1) + fib.apply(n - 2)
}
}
Итак, в Scala все функции являются объектами, реализующими черту FunctionX
, которая определяет метод с именем apply
. Как показано здесь и при создании списка выше, .apply
может быть опущено, что делает вызовы функций похожими на вызовы методов.
В конце концов, все в Scala является объектом - и экземпляром класса - и каждый такой объект действительно принадлежит классу, а весь код принадлежит методу, который каким-то образом выполняется. Даже match
в приведенном выше примере был методом, но был преобразован в ключевое слово, чтобы избежать некоторых проблем довольно давно.
Итак, как насчет его функциональной части? F # принадлежит к одному из самых традиционных семейств функциональных языков. Хотя у него нет некоторых функций, которые, по мнению некоторых людей, важны для функциональных языков, фактом является то, что F # является функцией, так сказать, по умолчанию .
Scala, с другой стороны, был создан с целью , объединяющей функциональные и ОО-модели, вместо того, чтобы просто предоставлять их как отдельные части языка. Степень успеха зависит от того, что вы считаете функциональным программированием. Вот некоторые вещи, на которых сосредоточился Мартин Одерский:
Функции являются значениями. Они также являются объектами - потому что все значения являются объектами в Scala - но концепция того, что функция - это значение, которым можно манипулировать, важна и имеет свои корни вплоть до исходной реализации Lisp.
Сильная поддержка неизменяемых типов данных. Функциональное программирование всегда было направлено на уменьшение побочных эффектов на программу, чтобы функции можно было анализировать как истинные математические функции. Таким образом, Scala позволил сделать вещи неизменными, но он не сделал двух вещей, за которые критики FP критикуют это:
- Это не делало изменчивость сложнее .
- Он не предоставляет систему эффектов , с помощью которой можно статически отслеживать изменчивость.
Поддержка алгебраических типов данных. Алгебраические типы данных (называемые ADT, что также сбивает с толку абстрактный тип данных, совсем другое) очень распространены в функциональном программировании и наиболее полезны в ситуациях, когда обычно используют шаблон посетителя в языках OO.
Как и все остальное, ADT в Scala реализованы как классы и методы, с некоторыми синтаксическими сахарами, которые делают их безболезненными для использования. Тем не менее, Scala гораздо более многословен, чем F # (или другие функциональные языки в этом отношении) в их поддержке. Например, вместо F # |
для операторов case он использует case
.
Поддержка нестрогости. Нестрогость означает только вычислительные вещи по требованию. Это существенный аспект Haskell, где он тесно интегрирован с системой побочных эффектов. Однако в Scala поддержка нестрогости довольно робкая и зарождающаяся. Он доступен и используется, но ограниченным образом.
Например, нестрогий список Scala, Stream
, не поддерживает действительно строгий foldRight
, как это делает Haskell. Более того, некоторые преимущества нестрогости достигаются только тогда, когда это значение по умолчанию в языке, а не опция.
Поддержка понимания списка. На самом деле, Scala называет это для понимания , поскольку способ его реализации полностью отделен от списков. В простейшем смысле, списочные выражения можно рассматривать как функцию / метод map
, показанную в примере, хотя вложенность операторов карты (поддерживается с flatMap
в Scala) и фильтрация (filter
или withFilter
) в Scala, в зависимости от требований строгости), как правило, ожидаются.
Это очень распространенная операция в функциональных языках, и она часто отличается синтаксисом - как в операторе in
в Python. Опять же, Scala более многословен, чем обычно.
На мой взгляд, Scala не имеет себе равных в сочетании FP и OO. Это происходит со стороны ОО спектра в сторону FP, что необычно. В основном, я вижу языки FP с ОО, которые рассматриваются на нем - и он чувствует себя решенным для меня. Я думаю, что FP на Scala, вероятно, чувствует себя так же для программистов на функциональных языках.
EDIT
Читая некоторые другие ответы, я понял, что есть еще одна важная тема: вывод типа. Лисп был динамически типизированным языком, и это в значительной степени устанавливало ожидания для функциональных языков. Все современные функциональные языки со статической типизацией имеют строгие системы логического вывода, чаще всего алгоритм Хиндли-Милнера 1 , который делает объявления типов по существу необязательными.
Scala не может использовать алгоритм Хиндли-Милнера из-за поддержки Scala наследования 2 . Поэтому Scala должен принять гораздо менее мощный алгоритм вывода типов - фактически вывод типов в Scala намеренно не определен в спецификации и является предметом постоянных улучшений (его усовершенствование является одной из важнейших функций в следующей версии 2.8). Скала, например).
В конце концов, однако, Scala требует, чтобы у всех параметров были объявлены их типы при определении методов. В некоторых ситуациях, таких как рекурсия, также должны быть объявлены типы возврата для методов.
Функции в Scala часто могут иметь свои типы выведенные вместо объявленных. Например, здесь не требуется объявления типа: List(1, 2, 3) reduceLeft (_ + _)
, где _ + _
на самом деле является анонимной функцией типа Function2[Int, Int, Int]
.
Аналогично, объявление типов переменных часто не нужно, но наследование может потребовать этого. Например, Some(2)
и None
имеют общий суперкласс Option
, но на самом деле принадлежат разным подклассам. Поэтому обычно нужно объявить var o: Option[Int] = None
, чтобы убедиться, что назначен правильный тип.
Эта ограниченная форма вывода типов намного лучше, чем обычно предлагают ОО-языки со статической типизацией, что дает Scala ощущение легкости, и намного хуже, чем обычно предлагают статически типизированные языки FP, что придает Scala ощущение тяжести. : -)
Примечания:
На самом деле, алгоритм происходит от Дамаса и Милнера, который назвал его «Алгоритм W», согласно википедии .
Мартин Одерский упомянул в комментарии здесь что:
Причина, по которой у Скалы нет вывода типа Хиндли / Милнера, заключается в
что это очень трудно сочетать с такими функциями, как
перегрузка (специальный вариант, не типовые классы), запись
выбор и подтип
Он продолжает заявлять, что это не может быть на самом деле невозможным, и дело дошло до компромисса. Пожалуйста, перейдите по этой ссылке для получения дополнительной информации, и, если у вас есть более четкое заявление или, еще лучше, какой-либо документ, так или иначе, я был бы признателен за справку.
Позвольте мне поблагодарить Джона Харропа за поиск этого, поскольку я предполагал, что это было невозможно . Ну, может быть, и я не смог найти подходящую ссылку. Однако обратите внимание, что проблема не в наследовании .