Algunos dicen que si lleva los principios SOLID a sus extremos, terminará en la programación funcional . Estoy de acuerdo con este artículo, pero creo que se pierde algo de semántica en la transición de la interfaz / objeto a la función / cierre, y quiero saber cómo la programación funcional puede mitigar la pérdida.
Del artículo:
Además, si aplica rigurosamente el Principio de segregación de interfaz (ISP), comprenderá que debe favorecer las interfaces de rol sobre las interfaces de encabezado.
Si sigue impulsando su diseño hacia interfaces cada vez más pequeñas, eventualmente llegará a la interfaz de roles definitiva: una interfaz con un solo método. Esto me pasa mucho. Aquí hay un ejemplo:
public interface IMessageQuery
{
string Read(int id);
}
Si tomo una dependencia de una IMessageQuery
parte del contrato implícito, esa llamada Read(id)
buscará y devolverá un mensaje con la ID dada.
Compare esto con tomar una dependencia de su firma funcional equivalente int -> string
. Sin señales adicionales, esta función podría ser simple ToString()
. Si lo implementaste IMessageQuery.Read(int id)
con un ToString()
¡puedo acusarlo de ser deliberadamente subversivo!
Entonces, ¿qué pueden hacer los programadores funcionales para preservar la semántica de una interfaz bien nombrada? ¿Es convencional, por ejemplo, crear un tipo de registro con un solo miembro?
type MessageQuery = {
Read: int -> string
}
fuente
Without any additional clues
... tal vez es por eso que la documentación es parte del contrato ?Respuestas:
Como dice Telastyn, comparando las definiciones estáticas de funciones:
a
Realmente no has perdido nada al pasar de OOP a FP.
Sin embargo, esto es solo una parte de la historia, porque las funciones y las interfaces no solo se mencionan en sus definiciones estáticas. También se pasan . Entonces, digamos que nuestro
MessageQuery
fue leído por otro código, aMessageProcessor
. Entonces tenemos:Ahora no podemos ver directamente el nombre del método
IMessageQuery.Read
o su parámetroint id
, pero podemos llegar muy fácilmente a través de nuestro IDE. En términos más generales, el hecho de que estamos pasando unaIMessageQuery
interfaz en lugar de cualquier interfaz con un método de una función de int a string significa que mantenemos losid
metadatos de nombre de parámetro asociados con esta función.Por otro lado, para nuestra versión funcional tenemos:
Entonces, ¿qué hemos guardado y perdido? Bueno, todavía tenemos el nombre del parámetro
messageReader
, lo que probablemente haceIMessageQuery
innecesario el nombre del tipo (el equivalente a ). Pero ahora hemos perdido el nombre del parámetroid
en nuestra función.Hay dos formas principales de evitar esto:
En primer lugar, al leer esa firma, ya puedes adivinar lo que sucederá. Al mantener las funciones cortas, simples y coherentes y al utilizar una buena nomenclatura, hace que sea mucho más fácil intuir o encontrar esta información. Una vez que comenzamos a leer la función en sí misma, sería aún más simple.
En segundo lugar, se considera un diseño idiomático en muchos lenguajes funcionales para crear tipos pequeños para envolver primitivas. En este caso, sucede lo contrario: en lugar de reemplazar un nombre de tipo con un nombre de parámetro (
IMessageQuery
tomessageReader
) podemos reemplazar un nombre de parámetro con un nombre de tipo. Por ejemplo,int
podría envolverse en un tipo llamadoId
:Ahora nuestra
read
firma se convierte en:Lo cual es tan informativo como lo que teníamos antes.
Como nota al margen, esto también nos proporciona parte de la protección del compilador que teníamos en OOP. Mientras que la versión OOP garantizaba que tomáramos específicamente una función anterior en
IMessageQuery
lugar de una simpleint -> string
, aquí tenemos una protección similar (pero diferente) que estamos tomando enId -> string
lugar de cualquier viejaint -> string
.Sería reacio a decir con 100% de confianza que estas técnicas siempre serán tan buenas e informativas como tener la información completa disponible en una interfaz, pero creo que de los ejemplos anteriores, puede decir que la mayoría de las veces, nosotros probablemente pueda hacer un trabajo igual de bueno.
fuente
Cuando hago FP, tiendo a usar tipos semánticos más específicos.
Por ejemplo, su método para mí se convertiría en algo como:
Esto comunica mucho más que el estilo de
ThingDoer.doThing()
estilo OO (/ java)fuente
read: MessageId -> Message
te dice questring MessageReader.GetMessage(int messageId)
no?Use funciones bien nombradas.
IMessageQuery::Read: int -> string
simplemente se convierteReadMessageQuery: int -> string
o algo similar.La clave a tener en cuenta es que los nombres son solo contratos en el sentido más amplio de la palabra. Solo funcionan si usted y otro programador deducen las mismas implicaciones del nombre y las obedecen. Debido a esto, realmente puede usar cualquier nombre que comunique ese comportamiento implícito. OO y la programación funcional tienen sus nombres en lugares ligeramente diferentes y en formas ligeramente diferentes, pero su función es la misma.
No en este ejemplo. Como expliqué anteriormente, una sola clase / interfaz con una sola función no es significativamente más informativa que una función independiente con un nombre similar.
Una vez que obtiene más de una función / campo / propiedad en una clase, puede inferir más información sobre ellos porque puede ver su relación. Es discutible si eso es más informativo que las funciones independientes que toman los mismos parámetros / parámetros similares o funciones independientes organizadas por espacio de nombres o módulo.
Personalmente, no creo que OO sea significativamente más informativo, incluso en ejemplos más complejos.
fuente
ReadMessageQuery id = <code to go fetch based on id>
let consumer (messageQuery : int -> string) = messageQuery 5
, elid
parámetro es solo unint
. Supongo que un argumento es que deberías pasar unId
, no unint
. De hecho, sería una respuesta decente en sí misma.No estoy de acuerdo con que una sola función no pueda tener un 'contrato semántico'. Considere estas leyes para
foldr
:¿En qué sentido no es semántico o no es un contrato? No es necesario que defina un tipo para un "plegador", especialmente porque
foldr
está determinado únicamente por esas leyes. Sabes exactamente lo que va a hacer.Si desea tomar una función de algún tipo, puede hacer lo mismo:
Solo necesita nombrar y capturar ese tipo si necesita el mismo contrato varias veces:
El verificador de tipos no impondrá la semántica que asigne a un tipo, por lo que crear un nuevo tipo para cada contrato es simplemente repetitivo.
fuente
foldr
. Sugerencia: la definición defoldr
tiene dos ecuaciones (¿por qué?), Mientras que la especificación dada anteriormente tiene tres.Casi todos los lenguajes funcionales de tipo estático tienen una forma de alias de tipos básicos de una manera que requiere que declares explícitamente tu intención semántica. Algunas de las otras respuestas han dado ejemplos. En la práctica, los programadores funcionales experimentados necesitan muy buenas razones para usar esos tipos de envoltorios, ya que perjudican la componibilidad y la reutilización.
Por ejemplo, supongamos que un cliente quería una implementación de una consulta de mensaje respaldada por una lista. En Haskell, la implementación podría ser tan simple como esta:
Usar
newtype Message = Message String
esto sería mucho menos sencillo, dejando esta implementación parecida a:Puede que no parezca un gran problema, pero tienes que hacer esa conversión de tipo de un lado a otro en todas partes , o configurar una capa límite en tu código donde todo lo que está arriba se
Int -> String
convierteId -> Message
para pasar a la capa de abajo. Digamos que quería agregar internacionalización, o formatearlo en mayúsculas, o agregar contexto de registro, o lo que sea. Esas operaciones son totalmente simples de componer con unInt -> String
, y molestas con unId -> Message
. No es que nunca haya casos en los que las restricciones de tipo aumentadas sean deseables, pero la molestia debería valer la pena.Puede usar un sinónimo de tipo en lugar de un contenedor (en Haskell en
type
lugar denewtype
), que es mucho más común y no requiere las conversiones en todo el lugar, pero tampoco proporciona las garantías de tipo estático como su versión OOP, solo un poco de enapsulación. Los envoltorios de tipos se usan principalmente cuando no se espera que el cliente manipule el valor, solo almacénelo y páselo de vuelta. Por ejemplo, un identificador de archivo.Nada puede evitar que un cliente sea "subversivo". Simplemente está creando aros para saltar, para cada cliente. Un simulacro simple para una prueba de unidad común a menudo requiere un comportamiento extraño que no tiene sentido en la producción. Sus interfaces deben estar escritas para que no les importe, si es posible.
fuente
Id
yMessage
son envoltorios simples paraInt
yString
, es trivial convertir entre ellos.