world una tutorial suma salto operator online logo listas lista linea leer hello funcion elem ejemplos descargar comandos ciclos haskell

una - salto de linea en haskell



Prevenir que un argumento sea un nĂºmero complejo (2)

Tengo una función como esta:

hypergeom :: forall a. (Eq a, Fractional a) => Int -- truncation weight -> a -- alpha parameter (usually 2) -> [a] -- "upper" parameters -> [a] -- "lower" parameters -> [a] -- variables (the eigen values) -> IO a hypergeom m alpha a b x = do ......

Elegí la restricción Fractional a porque quiero la posibilidad de tomar a tipo de Float , Double , Rational o Complex (por ejemplo, Complex Double o Complex Rational ).

Pero ahora, me gustaría permitir Complex excepto el parámetro alpha . Pero si a es el tipo Complex b entonces alpha debe ser del tipo b . Por ejemplo:

hypergeom :: => Int -- truncation weight -> Double -- alpha parameter (usually 2) -> [Complex Double] -- "upper" parameters -> [Complex Double] -- "lower" parameters -> [Complex Double] -- variables (the eigen values) -> IO (Complex Double)

Espero estar claro. ¿Cómo podría hacer eso de una manera ordenada?


Código

Si lo entiendo correctamente, podría usar una clase de tipo con una familia de tipos asociada para esto:

{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE DefaultSignatures #-} import Data.Complex import Data.Ratio class BaseFrac a where type family BaseFracType a type BaseFracType a = a -- Default type family instance (unless overridden) inject :: BaseFracType a -> a default inject :: BaseFracType a ~ a => BaseFracType a -> a inject = id instance Integral a => BaseFrac (Ratio a) instance BaseFrac Float instance BaseFrac Double -- etc... instance Num a => BaseFrac (Complex a) where type BaseFracType (Complex a) = a inject x = x :+ 0 hypergeom :: forall a. (Eq a, Fractional a, BaseFrac a) => Int -- truncation weight -> BaseFracType a -- alpha parameter (usually 2) -> [a] -- "upper" parameters -> [a] -- "lower" parameters -> [a] -- variables (the eigen values) -> IO a hypergeom m alpha a b x = ...

Es posible que necesite agregar métodos adicionales a la clase de tipo, pero creo que inject debería proporcionar alguna utilidad importante.

Explicación

Al escribir esta explicación, me di cuenta de que probablemente comprimí varias ideas en un área pequeña sin dar la información de fondo que debería haber dado. Espero que esto ayude y si tiene alguna pregunta o está confundido, ¡debe avisarme!

Aquí hay dos ideas principales para interactuar. El primero es el de una clase de tipo. Asumiré algunos antecedentes básicos sobre las clases de tipos (hay muchos recursos que repasan los conceptos básicos sobre eso. Si lo desea, puedo encontrar algunos para vincular aquí).

La otra es la idea de una familia tipográfica. Una familia de tipos es esencialmente una especie de función de tipos a tipos. A veces están dentro de clases de tipos (como están aquí), pero no tienen que estarlo. Además, a veces están "abiertos" y otras están "cerrados" (si están dentro de una clase de tipo, están esencialmente abiertos)

Familias de tipo cerrado

Creo que es instructivo mirar una familia de tipos cerrada que no está en una clase de tipo primero. Considera esto:

type family Example :: * -> * where Example Int = Bool Example a = a

Esto es muy parecido a una definición de función de Haskell normal, excepto que opera en tipos en lugar de valores. Si su entrada es del tipo Int , devuelve el tipo Bool . De lo contrario, devuelve el mismo tipo que el tipo que obtuvo como argumento.

Podemos ver esto usando :kind! comando en GHCi:

λ > :kind! Example Int Example Int :: * = Bool λ > λ > :kind! Example Char Example Char :: * = Char

También puede pensar en los sinónimos de tipo como una forma muy restringida de familia de tipos.

Ese tipo de familia se llama "cerrado" porque no puede agregar más "ecuaciones" a su definición (al igual que una función Haskell "normal").

Familias tipo abierto

Pero también puede tener familias de tipo "abierto" donde puede agregar ecuaciones adicionales más adelante. Por ejemplo:

type family OpenExample :: * -> * type instance OpenExample [a] = a type instance OpenExample Text = Char type instance OpenExample IntSet = Int -- ^ These just give you the "element type" inside some containers

Luego podemos agregar nuevas ecuaciones con type instance (por ejemplo, aquí si agregamos un nuevo tipo de contenedor).

Familias de tipos asociadas con clases de tipos

Esto nos lleva al tipo de familia de tipos que tenemos aquí: una clase de tipo con una familia de tipos asociada. Esto es muy parecido a una familia de tipo abierto, pero la entrada está restringida por la clase de tipo. Además, cada ecuación está dentro de una instancia de la clase de tipo.

He proporcionado una instancia de tipo predeterminada (la segunda línea de la class BaseFrac ) que se usará automáticamente si no se proporciona ninguna. Para escribir la instancia Double explícitamente (sin usar este valor predeterminado) se ve así:

instance BaseFrac Double where type BaseFracType Double = Double

Observe cuán similar es esto a la sintaxis de type instance .

También proporcioné una implementación predeterminada para el método de inject . Este valor predeterminado solo se puede usar si BaseFracType a es igual a a (esto es lo que significa la restricción BaseFracType a ~ a en la firma predeterminada).

Esta restricción se cumple para cualquier instance que use la definición predeterminada de BaseFracType (ya que es solo type BaseFracType a = a ), por lo que esas definiciones de instancia "vacías" simplemente funcionan automáticamente.

Entonces, para las instancias dadas hasta ahora, BaseFracType Double es lo mismo que Double (de la definición de familia de tipos (predeterminada) utilizada en la instancia Double de la clase BaseFrac ) y BaseFracType (Complex a) es lo mismo que a (del tipo definición de instancia familiar dada en el Complex a instancia de la clase BaseFrac ).

Para qué sirve inject

Eso explica por qué funcionan los tipos, pero las siguientes preguntas son: ¿cómo lo usamos realmente y por qué inject materia? Afortunadamente, las respuestas a esas dos preguntas están vinculadas.

inject esencialmente le proporciona una forma de poner un valor fraccional "básico" ("1-dimensional") en cualquier tipo que tenga una instancia de la clase BaseFrac .

Para la mayoría de los tipos, esta es solo la función de identidad (ya que Double ya es un valor fraccional "básico", etc.). Para el Complex a , esto es diferente. Simplemente construye un número complejo con un cero en su componente imaginario y su argumento como su componente real. En ese caso, es una función de tipo inject :: Num a => a -> Complex a .

Aquí hay un ejemplo simple de inject en acción basado en la función que asignó, con su generalidad completa (esta función funciona con cualquier entrada de BaseFrac ):

hypergeom :: forall a. (Eq a, Fractional a, BaseFrac a) => Int -> BaseFracType a -> [a] -> [a] -> [a] -> IO a hypergeom m alpha a b x = return (inject alpha * head a)

Si la variable tipo a es Rational , entonces:

  • alpha tiene tipo Rational (ya que BaseFracType Rational es lo mismo que Rational )
  • inject alpha también tiene tipo Rational
  • El valor de inject alpha es solo alpha

Si la variable de tipo a es Complex Double , entonces:

  • alpha tiene el tipo Double (ya que BaseFracType (Complex Double) es lo mismo que Double )
  • inject alpha tiene el tipo Complex Double
  • El valor de inject alpha es alpha :+ 0

También puedes usar el GHCi :kind! comando aquí:

λ > :kind! BaseFracType (Complex Double) BaseFracType (Complex Double) :: * = Double

Si hay algo que sea confuso, puede avisarme y debería poder aclararlo.

Material adicional

Hay más información sobre familias de tipos here . Probablemente las secciones más relevantes serían la sección sobre instancias de sinónimos de tipo (que son las familias de tipo de las que hablamos que no estaban asociadas con una clase de tipo) , la subsección sobre familias de tipo cerrado y la subsección sobre familias de tipo asociadas .

Tenga en cuenta que la página también habla sobre familias de datos, que no son particularmente relevantes aquí (las familias de datos son como GADT "abiertos").


Todos los Haskeller deben conocer la biblioteca de vector-space , y esta es una aplicación donde se puede usar.

hypergeom :: ∀ a. (VectorSpace a, Eq a, RealFrac (Scalar a)) => Int -- truncation weight -> Scalar a -- alpha parameter (usually 2) -> [a] -- "upper" parameters -> [a] -- "lower" parameters -> [a] -- variables (the eigen values) -> IO a hypergeom m α a b x = do ......

Esto usa, en el caso complejo ,

instance (RealFloat v, VectorSpace v) => VectorSpace (Complex v) where type Scalar (Complex v) = Scalar v s*^(u :+ v) = s*^u :+ s*^v

Sin embargo, advertencia: yo personalmente no soy fanático de esa instancia en particular. Debido a que los números complejos son un álgebra de división, a menudo es útil considerarlos como un tipo escalar , es decir

instance RealFloat a => VectorSpace (Complex a) where type Scalar (Complex a) = Complex a (*^) = (*)

La razón por la que esto es preferible es que los espacios vectoriales libres sobre el número complejo (por ejemplo, tuplas) serán en realidad espacios vectoriales complejos, no espacios vectoriales reales como lo son a partir de la versión 0.16 de la biblioteca.

Si la instancia se definiera como yo lo haría, entonces no funcionaría. Esto fue realmente discussed , tal vez cambie en el futuro.