haskell servant

haskell - Cómo agregar una restricción a un constructor de tipos



servant (1)

Considere el siguiente tipo de datos:

data Get (statusCode :: Nat)

En realidad, es un constructor de tipo simplificado de servidor que luego se usa en una API de nivel de tipo como esta:

type API = "users" :> Verb ''GET 200 ''[JSON] [User]

Para nuestros propósitos podemos reducir la API para

type API = Get 200

Ahora, tener una restricción del código de estado de tipo Nat es demasiado flojo, lo que permite definir un código de estado HTTP no existente:

type API = Get 999

Por lo tanto, la pregunta: ¿Hay una manera de restringir el conjunto de naturales que se pueden aplicar al constructor de tipo Get ?

Lo que se intentó

Omitiré todos los pragmas e importaciones en los ejemplos de código para mayor claridad.

Un tipo diferente para statusCode

Una forma obvia de solucionarlo sería definir un ADT separado para los códigos de estado y usarlo en lugar de Nat utilizando la promoción de tipos de datos.

data StatusCode = HTTP200 | HTTP201 | HTTP202 data Get (statusCode :: StatusCode)

Sin embargo, este es un cambio de última hora, que requeriría golpear una versión principal y volver a escribir todas las definiciones de los usuarios. Dudo que el beneficio de los códigos restringidos valga la pena.

DatatypeContexts

Esta extensión permite tener una restricción directa en nuestra variable de tipo

data IsStatusCode statusCode => Get (statusCode :: Nat)

pero requiere que los usuarios agreguen la restricción a toda su declaración. De nuevo, un cambio de ruptura. Además, DatatypeContexts está en desuso.

Tipo Familias

Podríamos crear condicionalmente Get'' partir del siguiente ejemplo utilizando familias de tipos, pero por alguna razón, la declaración de un alias de tipo se compila alegremente. Para obtener un error necesitamos construir un valor de este tipo, que también es un cambio importante.

data Get'' (statusCode :: Nat) = Get type family Get x where Get x = If (x <=? 600) (Get'' x) (TypeError (Text "Invalid Code")) type API1 = Get 200 type API2 = Get 999 -- compiles. api :: Get 999 -- doesn''t compile. api = Get


Comenzaré con una solución, luego discutiré las otras posibilidades (que parecen no haberse realizado):

{-# LANGUAGE TypeOperators, TypeInType, GADTs #-} import GHC.TypeLits (Nat, type (<=)) import Data.Proxy (Proxy(..)) import Data.Kind (type (*)) data Get (statusCode :: Nat) :: (statusCode <= 600) => * type API1 = Get 900 -- compiles type API2 = Get 200 -- compiles api1 :: Proxy (Get 900) -- doesn''t compile api1 = Proxy api2 :: Proxy (Get 200) -- compiles api2 = Proxy

No se necesitan familias de tipos, no hay necesidad de bajar nunca al nivel de valor. Los sinónimos de tipo, sin embargo, compilarán bien. El uso de un sinónimo de tipo de un Get no válido provocará un bloqueo del tiempo de compilación en el sitio de uso. Considero que esta es una solución tan buena como se puede esperar. Por favor, hágamelo saber si algo no está claro.

A continuación, solo algunas reflexiones sobre los otros enfoques:

DatatypeContexts

Éste nunca funcionará: además de estar en desuso, todo lo que hace es agregar la restricción a los constructores. Usted indicó específicamente que no quiere tener que construir nada del tipo, por lo que esto no tiene sentido. La nueva sintaxis de GADT corrige esta ambigüedad: las restricciones ahora están claramente vinculadas a los constructores de datos o tipos.

Tipo Familias y TypeError

Creo que esto debería funcionar, y sin tener que construir un valor del tipo. (Así que lo siguiente debería estar bien :)

data Get'' (statusCode :: Nat) type family Get x where Get x = If (x <=? 600) (Get'' x) (TypeError (Text "Invalid Code")) api2 :: Proxy (Get 200) -- should compile. api2 = Proxy api1 :: Proxy (Get 999) -- shouldn''t compile, but currently does api1 = Proxy

Estoy motivado para creer que esto debería funcionar en base al example de este marcador de rastreo. Claramente, algo aquí no se comporta como debería: incluso si sustituyo la llamada a la función de tipo If con el propio TypeError , aún no se activa nada, parece que los TypeErrors que no son de nivel superior todavía causan algunos problemas.

Por otro lado, no estoy realmente seguro de cuál debería ser el comportamiento de TypeError en los sinónimos de type , aunque me inclino a decir que el type API1 = Get 999 no debería compilarse, ya que type API1 = TypeError (Text "error") doesn '' t