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 tipoRational
(ya queBaseFracType Rational
es lo mismo queRational
) -
inject alpha
también tiene tipoRational
-
El valor de
inject alpha
es soloalpha
Si la variable de tipo
a
es
Complex Double
, entonces:
-
alpha
tiene el tipoDouble
(ya queBaseFracType (Complex Double)
es lo mismo queDouble
) -
inject alpha
tiene el tipoComplex Double
-
El valor de
inject alpha
esalpha :+ 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.