2010-06-06 9 views
5

Anteriormente solicité algunos comentarios sobre mi primer proyecto F #. Antes de cerrar la pregunta porque el alcance era demasiado grande, alguien tuvo la amabilidad de revisarlo y dejar un comentario.F #: ¿Ventajas de convertir funciones de nivel superior a métodos de miembro?

Una de las cosas que mencionaron fue señalar que tenía una serie de funciones regulares que podrían convertirse en métodos en mis tipos de datos. Diligentemente Fui a través de cambiar cosas como

let getDecisions hand = 
    let (/=/) card1 card2 = matchValue card1 = matchValue card2 
    let canSplit() = 
     let isPair() = 
      match hand.Cards with 
      | card1 :: card2 :: [] when card1 /=/ card2 -> true 
      | _ -> false 
     not (hasState Splitting hand) && isPair() 
    let decisions = [Hit; Stand] 
    let split = if canSplit() then [Split] else [] 
    let doubleDown = if hasState Initial hand then [DoubleDown] else [] 
    decisions @ split @ doubleDown 

a esto:

type Hand 
// ...stuff... 
member hand.GetDecisions = 
    let (/=/) (c1 : Card) (c2 : Card) = c1.MatchValue = c2.MatchValue 
    let canSplit() = 
     let isPair() = 
      match hand.Cards with 
      | card1 :: card2 :: [] when card1 /=/ card2 -> true 
      | _ -> false 
     not (hand.HasState Splitting) && isPair() 
    let decisions = [Hit; Stand] 
    let split = if canSplit() then [Split] else [] 
    let doubleDown = if hand.HasState Initial then [DoubleDown] else [] 
    decisions @ split @ doubleDown 

Ahora, no me cabe duda que soy un idiota, pero aparte de (supongo) haciendo más fácil interoperabilidad C# ¿Qué me ganó eso? Específicamente, encontré un par de ventajas dis, sin contar el trabajo extra de conversión (que no contaré, ya que podría haberlo hecho de esta manera, en primer lugar, supongo, aunque eso hubiera sido posible usando F # Interactive más de un dolor). Por un lado, ahora ya no puedo trabajar con la función "pipelining" fácilmente. Tenía que ir y cambiar some |> chained |> calls a (some |> chained).Calls etc. Además, parecía hacer que mi sistema de tipos fuera más tonto, mientras que con mi versión original, mi programa no necesitaba anotaciones de tipo, después de convertirlo en gran parte en métodos de miembros, tuve un montón de errores sobre las búsquedas son indeterminadas en ese punto, y tuve que ir y agregar anotaciones tipo (un ejemplo de esto está en el (/=/) anterior).

Espero no haber salido demasiado dudoso, ya que agradezco el consejo que recibí, y escribir código idiomático es importante para mí. Solo tengo curiosidad por qué la expresión es la forma en que es :)

Gracias!

Respuesta

4

Una ventaja de los miembros es Intellisense y otra herramienta que hace que los miembros de detección. Cuando un usuario desea explorar un objeto foo, puede escribir foo. y obtener una lista de los métodos del tipo. Los miembros también 'escalan' más fácilmente, en el sentido de que no terminan con docenas de nombres flotando en el nivel superior; a medida que crece el tamaño del programa, necesita más nombres para estar disponible solo cuando está calificado (algún método Obj o algún tipo de espacio de nombres o algún tipo de archivo .func, en lugar de simplemente método/tipo/función 'flotante libre').

Como has visto, también hay desventajas; la inferencia tipo es especialmente notable (necesita saber el tipo de x a priori para llamar al x.Something); en el caso de tipos y funcionalidades que se usan con mucha frecuencia, puede ser útil proporcionar tanto miembros como un módulo de funciones para obtener los beneficios de ambos (esto es, por ejemplo, lo que ocurre con los tipos de datos comunes en FSharp.Core).

Estas son las ventajas y desventajas típicas de la "comodidad del guión" frente a la "escala de ingeniería del software". Personalmente, siempre me inclino por este último.

3

Esta es una pregunta difícil y ambos enfoques tienen ventajas y desventajas. En general, tiendo a usar miembros cuando escribo más código orientado a objetos/estilo C# y funciones globales cuando escribo un código más funcional. Sin embargo, no estoy seguro de poder indicar claramente cuál es la diferencia.

yo preferiría el uso de funciones globales al escribir:

  • tipos de datos funcionales tales como árboles/listas y otras estructuras de datos utilizables en general que mantienen una cierta cantidad mayor de datos en su programa. Si su tipo proporciona algunas operaciones que se expresan como funciones de orden superior (por ejemplo, List.map), es probable que sea este tipo de tipo de datos.

  • Funcionalidad no relacionada con el tipo - hay situaciones en las que algunas operaciones no pertenecen estrictamente a un tipo en particular. Por ejemplo, cuando tiene dos representaciones de alguna estructura de datos y está implementando la transformación entre las dos (por ejemplo, AST con tipo y AST sin tipo en un compilador). En ese caso, las funciones son una mejor opción.

Por otro lado, me gustaría utilizar miembros al escribir:

  • tipos simples como Card que puede tener propiedades (como valor y color) y relativamente simple (o no) métodos que realizan algunos cálculos.

  • orientada a objetos - cuando se necesita para utilizar conceptos orientados a objetos, como las interfaces, a continuación, tendrá que escribir la funcionalidad como miembros, lo que puede ser útil tener en cuenta (por adelantado) si esta será el caso para tus tipos.

También me gustaría mencionar que es perfectamente posible utilizar los miembros para algunos tipos sencillos en su aplicación (por ejemplo Card) y aplicar el resto de la aplicación que utiliza funciones de nivel superior. De hecho, creo que este sería probablemente el mejor enfoque en su situación.

Cabe destacar que también es posible crear un tipo que tenga miembros junto con un módulo que proporcione la misma funcionalidad que las funciones. Esto permite al usuario de la biblioteca elegir el estilo que prefiera y este es un enfoque completamente válido (sin embargo, probablemente tenga sentido para las bibliotecas independientes).

Como nota lateral, el encadenamiento de llamadas se puede lograr usando ambos miembros y funciones:

// Using LINQ-like extension methods 
list.Where(fun a -> a%3 = 0).Select(fun a -> a * a) 
// Using F# list-processing functions 
list |> List.filter (fun a -> a%3 = 0) |> List.map (fun a -> a * a) 
+1

molesto, no puede poner cada llamada de método en una nueva línea usando la notación de punto. –

7

Una práctica que utilicé en mi F # programación es utilizar el código en ambos lugares.

La implementación real es natural para ser puesto en un módulo:

module Hand = 
    let getDecisions hand = 
    let (/=/) card1 card2 = matchValue card1 = matchValue card2 
    ... 

y dar un 'enlace' en la función de miembro/propiedad:

type Hand 
    member this.GetDecisions = Hand.getDecisions this 

Esta práctica también se utiliza generalmente en otras bibliotecas F #, por ejemplo la matriz y la implementación del vector matrix.fs en powerpack.

En la práctica, no todas las funciones se deben colocar en ambos lugares. La decisión final debe basarse en el conocimiento del dominio.

En cuanto al nivel superior, la práctica es poner funciones en los módulos, y hacer algunos de ellos en el nivel superior si es necesario. P.ej. en F # PowerPack, Matrix<'T> está en el espacio de nombres Microsoft.FSharp.Math. Es conveniente hacer un acceso directo para el constructor de matriz en el nivel superior de manera que el usuario puede construir una matriz directamente sin abrir el espacio de nombres:

[<AutoOpen>] 
module MatrixTopLevelOperators = 
    let matrix ll = Microsoft.FSharp.Math.Matrix.ofSeq ll 
1

Generalmente evito las clases principalmente debido a la forma en que los miembros paralizan la inferencia de tipo. Si hace esto, a veces es útil hacer que el nombre del módulo sea el mismo que el del tipo: la siguiente es la siguiente.

type Hand 
// ...stuff... 

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] 
module Hand = 
    let GetDecisions hand = //... 
+0

¿Podría ampliar un poco sobre lo que hace ese atributo? –

+0

Los módulos se compilan en clases .NET, por lo que normalmente no se puede tener 'type Hand' y' module Hand' en el mismo espacio de nombres. El atributo anterior hace que la clase del módulo se denomine 'HandModule' en su lugar (en lo que se refiere al tiempo de ejecución de .NET), al tiempo que permite que el código F # use la sintaxis' Hand.GetDecisions'. –

Cuestiones relacionadas