languages - Pregunta Scala versus F#: ¿cómo unifican los paradigmas OO y FP?
functional programming python (7)
Hay bastantes puntos que puede usar para comparar los dos (o tres). Primero, he aquí algunos puntos notables que puedo pensar:
Sintaxis
Sintácticamente, F # y OCaml se basan en la tradición de programación funcional (espacio separado y más ligero), mientras que Scala se basa en el estilo orientado a objetos (aunque Scala lo hace más liviano).Integrando OO y FP
Tanto F # como Scala integran sin problemas OO con FP (¡porque no hay contradicción entre estos dos!) Puede declarar clases para mantener datos inmutables (aspecto funcional) y proporcionar miembros relacionados con el trabajo con los datos, también puede usar interfaces para abstracción (aspectos orientados a objetos). No estoy tan familiarizado con OCaml , pero creo que pone más énfasis en el lado OO (en comparación con F #)Estilo de programación en F #
Creo que el estilo de programación habitual utilizado en F # (si no necesita escribir la biblioteca .NET y no tiene otras limitaciones) es probablemente más funcional y usaría las características OO solo cuando lo necesite. Esto significa que agrupa la funcionalidad usando funciones, módulos y tipos de datos algebraicos.Estilo de programación en Scala
En Scala, el estilo de programación predeterminado está más orientado a objetos (en la organización), sin embargo, usted todavía (probablemente) escribe programas funcionales, porque el enfoque "estándar" es escribir código que evite la mutación.
¿Cuáles son las diferencias clave entre los enfoques adoptados por Scala y F # para unificar los paradigmas OO y FP?
EDITAR
¿Cuáles son los méritos y deméritos relativos de cada enfoque? Si, a pesar del soporte para la subtipificación, F # puede inferir los tipos de argumentos de funciones, entonces ¿por qué Scala no?
La sintaxis de F # se tomó de OCaml pero el modelo de objetos de F # se tomó de .NET. Esto le da a F # una sintaxis clara y concisa que es característica de los lenguajes de programación funcionales y al mismo tiempo permite que F # interopere con los lenguajes .NET existentes y las librerías .NET sin problemas a través de su modelo de objetos.
Scala hace un trabajo similar en la JVM que F # hace en la CLR. Sin embargo, Scala ha elegido adoptar una sintaxis similar a Java. Esto puede ayudar a su adopción por programadores orientados a objetos, pero para un programador funcional puede sentirse un poco pesado. Su modelo de objetos es similar a Java, lo que permite una interoperación perfecta con Java, pero tiene algunas diferencias interesantes, como el soporte de rasgos.
Si la programación funcional significa programación con funciones , entonces Scala lo dobla un poco. En Scala, si entiendo correctamente, estás programando con métodos en lugar de funciones.
Cuando la clase (y el objeto de esa clase) detrás del método no importa, Scala te permitirá fingir que es solo una función. Tal vez un abogado de Scala puede elaborar sobre esta distinción (si es que es una distinción) y sobre cualquier consecuencia.
F # es funcional: permite OO bastante bien, pero el diseño y la filosofía son funcionales. Ejemplos:
- Funciones al estilo Haskell
- Currying automático
- Genéricos automáticos
- Escriba inferencia para argumentos
Se siente relativamente torpe utilizar F # de una manera principalmente orientada a objetos, por lo que uno podría describir el objetivo principal como integrar OO en la programación funcional .
Scala es multi-paradigma con enfoque en la flexibilidad. Puede elegir entre FP, POO y estilo de procedimiento auténticos según lo que mejor se adapte actualmente. Se trata de unificar OO y programación funcional .
De este artículo sobre Lenguajes de Programación:
Scala es un reemplazo robusto, expresivo y estrictamente superior para Java. Scala es el lenguaje de programación que utilizaría para una tarea como escribir un servidor web o un cliente de IRC. A diferencia de OCaml [o F #], que era un lenguaje funcional con un sistema orientado a objetos injertado, Scala se siente más como un verdadero híbrido de programación orientada a objetos y funcional. (Es decir, los programadores orientados a objetos deberían poder comenzar a usar Scala inmediatamente, recogiendo las partes funcionales solo como lo deseen).
Aprendí por primera vez sobre Scala en POPL 2006 cuando Martin Odersky dio una charla invitada sobre el tema. En ese momento, veía la programación funcional como estrictamente superior a la programación orientada a objetos, por lo que no veía la necesidad de un lenguaje que fusionara la programación funcional y orientada a objetos. (Probablemente fue porque todo lo que escribí en aquel entonces eran compiladores, intérpretes y analizadores estáticos).
La necesidad de Scala no se hizo aparente para mí hasta que escribí un HTTPD concurrente desde cero para soportar AJAX largamente sondeado para yaplet. Para obtener un buen soporte multinúcleo, escribí la primera versión en Java. Como lenguaje, no creo que Java sea tan malo, y puedo disfrutar de una programación bien orientada a objetos. Como programador funcional, sin embargo, la falta de soporte (o innecesariamente detallado) de funciones de programación funcional (como funciones de orden superior) me irrita cuando programo en Java. Entonces, le di una oportunidad a Scala.
Scala se ejecuta en la JVM, por lo que pude convertir gradualmente mi proyecto existente en Scala. También significa que Scala, además de su propia biblioteca bastante grande, también tiene acceso a toda la biblioteca de Java. Esto significa que puede hacer un trabajo real en Scala.
Cuando comencé a usar Scala, quedé impresionado por la inteligencia con que los mundos funcionales y orientados a objetos se mezclaron. En particular, Scala tiene un poderoso sistema de combinación de patrones y clases de casos que solucionó los inconvenientes persistentes de mis experiencias con Standard ML, OCaml y Haskell: el programador puede decidir qué campos de un objeto deben coincidir (en lugar de forzarlos a coincidir) en todos ellos) y se permiten argumentos de aria variable. De hecho, Scala incluso permite patrones definidos por el programador. Escribo muchas funciones que operan en nodos de sintaxis abstractos, y es agradable poder hacer coincidir solo a los niños sintácticos, pero todavía tengo campos para cosas como anotaciones o líneas en el programa original. El sistema de clases de casos permite dividir la definición de un tipo de datos algebraicos en varios archivos o en varias partes del mismo archivo, lo cual es notablemente útil.
Scala también admite herencia múltiple bien definida a través de dispositivos de clase llamados rasgos.
Scala también permite un grado considerable de sobrecarga; incluso la aplicación de la función y la actualización de la matriz pueden estar sobrecargadas. En mi experiencia, esto hace que mis programas de Scala sean más intuitivos y concisos.
Una característica que resulta ahorrar mucho código, de la misma manera que las clases de tipo guardar código en Haskell, es implícita. Puede imaginar implicaciones como API para la fase de recuperación de errores del verificador de tipos. En resumen, cuando el verificador de tipos necesita una X pero obtiene una Y, verificará si hay una función implícita en el alcance que convierta Y en X; si encuentra uno, "lanza" usando lo implícito. Esto hace que parezca que está extendiendo casi cualquier tipo de Scala, y permite incrustaciones más estrictas de DSL.
Del extracto de arriba es claro que el enfoque de Scala para unificar paradigmas OO y FP es mucho más superior al de OCaml o F #.
HTH.
Saludos,
Eric.
¿Cuáles son las diferencias clave entre los enfoques adoptados por Scala y F # para unificar los paradigmas OO y FP?
La diferencia clave es que Scala intenta mezclar los paradigmas haciendo sacrificios (generalmente en el lado de FP), mientras que F # (y OCaml) generalmente trazan una línea entre los paradigmas y permiten que el programador los elija entre ellos para cada tarea.
Scala tuvo que hacer sacrificios para unificar los paradigmas. Por ejemplo:
Las funciones de primera clase son una característica esencial de cualquier lenguaje funcional (ML, Scheme y Haskell). Todas las funciones son de primera clase en F #. Las funciones de miembro son de segunda clase en Scala.
La sobrecarga y los subtipos impiden la inferencia de tipo. F # proporciona una gran sublengua que sacrifica estas características de OO para proporcionar inferencias de tipo potentes cuando estas características no se utilizan (que requieren anotaciones de tipo cuando se usan). Scala implementa estas funciones en todas partes para mantener un OO consistente a costa de la inferencia de tipo pobre en todas partes.
Otra consecuencia de esto es que F # se basa en ideas probadas y comprobadas, mientras que Scala es pionera en este aspecto. Esto es ideal para las motivaciones detrás de los proyectos: F # es un producto comercial y Scala es investigación de lenguaje de programación.
Como un aparte, Scala también sacrificó otras características principales de FP como la optimización de la cola de llamadas por razones pragmáticas debido a las limitaciones de su VM de elección (la JVM). Esto también hace que Scala sea mucho más OOP que FP. Tenga en cuenta que hay un proyecto para llevar Scala a .NET que usará el CLR para realizar un TCO genuino.
¿Cuáles son los méritos y deméritos relativos de cada enfoque? Si, a pesar del soporte para la subtipificación, F # puede inferir los tipos de argumentos de funciones, entonces ¿por qué Scala no?
La inferencia de tipo está en desacuerdo con las características centradas en OO, como la sobrecarga y los subtipos. F # eligió la inferencia de tipo sobre la coherencia con respecto a la sobrecarga. Scala eligió sobrecargas y subtipos ubicuos sobre la inferencia de tipos. Esto hace que F # sea más como OCaml y Scala más como C #. En particular, Scala ya no es un lenguaje de programación funcional más que C #.
Lo que es mejor es completamente subjetivo, por supuesto, pero personalmente prefiero la tremenda brevedad y claridad que proviene de la inferencia de tipo poderoso en el caso general. OCaml es un lenguaje maravilloso, pero un punto de dolor fue la falta de sobrecarga del operador que requería programadores para usar +
para ints, +.
para flotadores, +/
para racionales, etc. Una vez más, F # elige el pragmatismo por encima de la obsesión al sacrificar la inferencia de tipo por sobrecargar específicamente en el contexto de los numéricos, no solo en operadores aritméticos sino también en funciones aritméticas como el sin
. Cada esquina del lenguaje F # es el resultado de compromisos pragmáticos cuidadosamente seleccionados como este. A pesar de las inconsistencias resultantes, creo que esto hace que F # sea mucho más útil.
He visto F #, haciendo tutoriales de bajo nivel, por lo que mi conocimiento es muy limitado. Sin embargo, era evidente para mí que su estilo era esencialmente funcional, con OO más como un complemento, mucho más un sistema de módulo ADT + que verdadero OO. La sensación que tengo se puede describir mejor como si todos los métodos en ella fueran estáticos (como en Java estático).
Ver, por ejemplo, cualquier código que use el operador de tubería ( |>
). Toma este fragmento de la entrada de wikipedia en F # :
[1 .. 10]
|> List.map fib
(* equivalent without the pipe operator *)
List.map fib [1 .. 10]
El map
función no es un método de la instancia de la lista. En cambio, funciona como un método estático en un módulo de List
que toma una instancia de lista como uno de sus parámetros.
Scala, por otro lado, es completamente OO. Comencemos, primero, con el equivalente de Scala de ese código:
List(1 to 10) map fib
// Without operator notation or implicits:
List.apply(Predef.intWrapper(1).to(10)).map(fib)
Aquí, el map
es un método en la instancia de List
. Los métodos de tipo estático, como intWrapper
en intWrapper
o apply
en List
, son mucho más raros. Luego hay funciones, como fib
arriba. Aquí, fib
no es un método en int
, pero tampoco es un método estático. En cambio, es un objeto , la segunda diferencia principal que veo entre F # y Scala.
Consideremos la implementación F # de la Wikipedia y una implementación de Scala equivalente:
// 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)
}
La implementación anterior de Scala es un método, pero Scala lo convierte en una función para poder pasarlo al map
. Lo modificaré a continuación para que se convierta en un método que devuelva una función, para mostrar cómo funcionan las funciones en 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)
}
}
Entonces, en Scala, todas las funciones son objetos que implementan el rasgo FunctionX
, que define un método llamado apply
. Como se muestra aquí y en la lista de creación anterior, se puede omitir .apply
, lo que hace que las llamadas a funciones se vean como llamadas de método.
Al final, todo en Scala es un objeto, y una instancia de una clase, y cada uno de esos objetos pertenece a una clase, y todos los códigos pertenecen a un método, que se ejecuta de alguna manera. Incluso la match
en el ejemplo anterior solía ser un método, pero se ha convertido en una palabra clave para evitar algunos problemas hace bastante tiempo.
Entonces, ¿qué hay de la parte funcional de esto? F # pertenece a una de las familias más tradicionales de lenguajes funcionales. Si bien no tiene algunas características que algunas personas piensan que son importantes para los lenguajes funcionales, el hecho es que F # funciona por defecto , por así decirlo.
Scala, por otro lado, se creó con la intención de unificar modelos funcionales y OO, en lugar de simplemente proporcionarlos como partes separadas del lenguaje. El grado en que fue exitoso depende de lo que usted considere que es una programación funcional. Estas son algunas de las cosas en las que se centró Martin Odersky:
Las funciones son valores. También son objetos, porque todos los valores son objetos en Scala, pero el concepto de que una función es un valor que puede manipularse es muy importante, y sus raíces se remontan a la implementación original de Lisp.
Soporte fuerte para tipos de datos inmutables. La programación funcional siempre se ha preocupado por disminuir los efectos secundarios en un programa, que las funciones se pueden analizar como verdaderas funciones matemáticas. Entonces Scala hizo que sea fácil hacer las cosas inmutables, pero no hizo dos cosas por las que los puristas de FP lo critican:
- No hizo la mutabilidad más difícil .
- No proporciona un sistema de efectos , por el cual la mutabilidad puede rastrearse estáticamente.
Soporte para tipos de datos algebraicos. Los tipos de datos algebraicos (llamados ADT, que confusamente también significa Tipo de datos abstractos, una cosa diferente) son muy comunes en la programación funcional, y son más útiles en situaciones donde uno comúnmente usa el patrón de visitante en los lenguajes OO.
Al igual que con todo lo demás, los ADT en Scala se implementan como clases y métodos, con algunos azúcares sintácticos para que sean fáciles de usar. Sin embargo, Scala es mucho más detallado que F # (u otros lenguajes funcionales, para el caso) en el apoyo de ellos. Por ejemplo, en lugar de F # ''s
|
para las declaraciones de casos, usacase
ycase
.Soporte para la no severidad. No estricta significa solo computar cosas bajo demanda. Es un aspecto esencial de Haskell, donde está estrechamente integrado con el sistema de efectos secundarios. En Scala, sin embargo, el apoyo no estricto es bastante tímido e incipiente. Está disponible y se usa, pero de forma restringida.
Por ejemplo, la lista no estricta de Scala,
Stream
, no admite unfoldRight
realmente no estricto, como lo hace Haskell. Además, algunos beneficios de no rigurosidad solo se obtienen cuando es el valor predeterminado en el idioma, en lugar de una opción.Soporte para la comprensión de la lista. En realidad, Scala lo llama : comprensión , ya que la forma en que se implementa está completamente divorciada de las listas. En sus términos más simples, la lista de comprensiones puede considerarse como la función / método del
map
que se muestra en el ejemplo, aunque anidando enunciados de mapa (compatible conflatMap
en Scala) y filtrando (filter
o conwithFilter
en Scala, dependiendo de los requisitos de rigor) usualmente se esperan.Esta es una operación muy común en lenguajes funcionales, y a menudo ligera en sintaxis, como en el operador de Python. De nuevo, Scala es algo más detallado de lo habitual.
En mi opinión, Scala no tiene parangón al combinar FP y OO. Viene del lado OO del espectro hacia el lado FP, lo cual es inusual. Sobre todo, veo lenguajes de FP con OO abordados en él, y me parece que se me ha tratado. Supongo que FP en Scala probablemente se sienta de la misma manera para los programadores de idiomas funcionales.
EDITAR
Al leer algunas otras respuestas, me di cuenta de que había otro tema importante: escribir inferencia. Lisp era un lenguaje tipado dinámicamente, y eso prácticamente establecía las expectativas para los lenguajes funcionales. Los lenguajes funcionales modernos de tipo estático tienen todos sistemas de inferencia de tipo fuerte, muy a menudo el algoritmo Hindley-Milner 1 , que hace que las declaraciones de tipo sean esencialmente opcionales.
Scala no puede usar el algoritmo de Hindley-Milner debido a la compatibilidad de Scala con la herencia 2 . Entonces, Scala tiene que adoptar un algoritmo de inferencia de tipo mucho menos poderoso: de hecho, la inferencia de tipo en Scala está intencionalmente indefinida en la especificación y es objeto de mejoras en curso (su mejora es una de las características más importantes de la próxima versión 2.8 de Scala, por ejemplo).
Al final, sin embargo, Scala requiere que todos los parámetros tengan sus tipos declarados al definir los métodos. En algunas situaciones, como la recursión, también se deben declarar los tipos de devolución para los métodos.
Sin embargo, las funciones en Scala a menudo pueden tener sus tipos inferidos en lugar de declarados. Por ejemplo, no es necesaria ninguna declaración de tipo aquí: List(1, 2, 3) reduceLeft (_ + _)
, donde _ + _
es en realidad una función anónima de tipo Function2[Int, Int, Int]
.
Del mismo modo, el tipo de declaración de variables a menudo es innecesario, pero la herencia puede requerirlo. Por ejemplo, Some(2)
y None
tienen una Option
superclase común, pero en realidad pertenecen a diferentes subclases. Por lo tanto, uno generalmente declararía var o: Option[Int] = None
para asegurarse de que se haya asignado el tipo correcto.
Esta forma limitada de inferencia de tipo es mucho mejor que los lenguajes OO tipicamente tipificados que generalmente se ofrecen, lo que le da a Scala una sensación de ligereza, y mucho peor que los lenguajes FP tipificados de forma estática, lo que le da a Scala una sensación de pesadez. :-)
Notas:
En realidad, el algoritmo proviene de Damas y Milner, que lo llamaron "Algoritmo W", según la wikipedia .
Martin Odersky mencionó en un comentario aquí que:
El motivo por el cual Scala no tiene inferencia de tipo Hindley / Milner es que es muy difícil combinarlo con características como la sobrecarga (la variante ad-hoc, no las clases de tipo), la selección de registros y la subtipificación
Continúa afirmando que puede no ser realmente imposible, y se redujo a un intercambio. Diríjase a ese enlace para obtener más información y, si tiene una declaración más clara o, mejor aún, un documento de una forma u otra, le agradecería la referencia.
Permítanme agradecerle a Jon Harrop por buscar esto, ya que estaba asumiendo que era imposible . Bueno, tal vez es así, y no pude encontrar un enlace adecuado. Tenga en cuenta, sin embargo, que no es solo la herencia la que causa el problema.