2012-04-13 22 views
12

Considere un tipo de fecha y hora donde la fecha debe estar presente, pero la parte de tiempo en segundos es opcional. Si la parte de tiempo está allí, también puede haber una parte de milisegundos opcional. Si hay milisegundos, también puede haber una parte de nanosegundos.Datos "dependientes opcionales" en Haskell

Hay muchas maneras de hacer frente a esto, por ejemplo .:

--rely on smart constructors 
data DateTime = DateTime { days:: Int, 
          sec :: Maybe Int, 
          ms :: Maybe Int, 
          ns :: Maybe Int 
         } 

-- list all possibilities 
data DateTime = DateOnly Int 
       | DateWithSec Int Int 
       | DateWithMilliSec Int Int Int 
       | DateWithNanoSec Int Int Int Int  

-- cascaded Maybe 
data DateTime = DateTime Int (Maybe (Int, Maybe (Int, Maybe Int))) 

-- cascaded data 
data Nano = NoNano | Nano Int 
data MilliSec = NoMilliSec | MilliSec Int Nano 
data Sec = NoSec | Sec Int MilliSec 
data Date = Date Int Sec 

que construyen usaría (por supuesto no se limita a los ejemplos anteriores), y por qué?

[intenciones]

estoy explorando las posibilidades de un tipo de fecha en Frege (http://code.google.com/p/frege/), utilizando date4j de DateTime como una línea guía (como la fecha y la hora lib de Haskell es demasiado complicado, y java.util.Date demasiado roto). En mi implementación actual de juguetes, todos los campos son obligatorios, pero, por supuesto, sería bueno liberar al usuario de la precisión no deseada (y la implementación original tiene campos opcionales).

Así que los principales objetivos son:

  • seguridad: estados ilegales deben ser evitados a toda costa
  • conveniencia: Debe ser fácil de trabajar con el tipo, por ejemplo, la coincidencia de patrones estaría bien, los cálculos del calendario debe ser fácil ...

No tan importantes son:

  • rendimiento: Por supuesto trabajar con el tipo no debe ser demasiado lento, pero para la típica uso no tiene que sqeeze el último ciclo de reloj
  • memoria: en los casos en que esto realmente importa, sería fácil derivar un formato de almacenamiento más compacto
  • implementación concisa: Es una biblioteca, y estoy dispuesto a agregar todo el código necesario para que las cosas se suavicen

Dicho esto, todo esto es muy tentativo y no debe tomarse demasiado en serio.

+1

No estoy seguro de que esto responda sin saber más acerca de sus requisitos. Puede depender de muchas cosas, incluidas las funciones que planea definir sobre el tipo, el rendimiento y las consideraciones de espacio (cantidad de indirecciones, oportunidades para desempaquetar campos, etc.) – hammar

+2

Otra opción es usar un constructor simple 'DateTime = DateTime [Int ] 'que no se exporta de la biblioteca, y forzar toda interacción con el tipo a través de funciones como' mkDateTimeWithSec :: [Int] -> DateTime', 'isDateTimeWithSec :: DateTime -> Bool',' getSeconds :: DateTime -> Maybe Int', etc. No puede usar la coincidencia de patrones (pero puede que no sea terrible, ya que solo lo reemplaza con guardias y utiliza las funciones de acceso) pero explícitamente no permite a sus usuarios crear estados ilegales, ya que puede verificar todo en el momento de la construcción (por ejemplo, puede verificar valores negativos). –

+1

La afirmación original de que los datos no especificados son opcionales en lugar de predeterminados lo convierte en un tipo muy artificial, las funciones de escritura para manipularlo serán tediosas. También puede tener como un valor basura con solo nanosegundos, pero nada segundos. Cambiaría la táctica y averiguaría si el incumplimiento de 0 para valores de tiempo no especificados es perjudicial en teoría y en práctica. –

Respuesta

10

(Esto no es una respuesta, pero es demasiado largo para un comentario y será más claro aquí.)

Hay otra forma que se podía manejar la situación: tienen un solo tipo DateTime que almacena todos los campos siempre junto con un parámetro que representa la precisión, por ejemplo

data Precision = Days | Seconds | Milliseconds | Nanoseconds deriving (Ord, Eq {- etc -}) 
data DateTime = DateTime { prec :: Precision, 
          days :: Int, 
          sec :: Int, 
          ms :: Int, 
          ns :: Int } 

Y el uso de constructores inteligentes que establecen los parámetros utilizados para 0. Si tiene dateDifference o lo que sea, puede propagar la precisión a través de (la instancia Ord lo haría limpio).

(Tengo poca idea de lo bueno/Haskell-y esto es, pero las otras soluciones parecen bastante desordenado, tal vez esto es más elegante.)

+1

Estaba a punto de sugerir este tipo de solución. – augustss

+1

Este es el tipo de cosas que habría sugerido, pero también habría sugerido simplemente colapsar los campos 'sec',' ms', y 'ns' en un solo campo' withinDay' cuya interpretación sería en segundos, milisegundos, o nanosegundos dependiendo del valor del campo 'prec'. –

+2

@DanielWagner, no creo que funcione, no sin cambiar los tipos de datos: (para una plataforma de 32 bits) el valor máximo de un 'Int 'es de unos pocos miles de millones, pero hay 86 billones de nanosegundos en un día. – huon

8

“estados ilegales deben ser evitados a toda costa” y "la coincidencia de patrones sería genial" son buenos principios que en este caso están en conflicto directo entre sí.

Además, las fechas y los tiempos son construcciones humanas humanas gnarly con muchos bordes y rincones irregulares. No son el tipo de reglas que podemos codificar fácilmente en el sistema de tipos.

Así que en este caso iría con un tipo de datos opaco, constructores inteligentes y deconstructors inteligentes. Siempre hay patrones de vista y patrones de protección para las ocasiones en que queremos usar la coincidencia de patrones.

(Y ni siquiera he discutido datos opcionales dependientes como un factor de motivación.)

+0

Los patrones de vista serían muy útiles aquí, pero actualmente no son compatibles con mi idioma de destino Frege (que de otra manera es muy similar a Haskell) – Landei

+0

Eso es desafortunado. Creo que iría de esta manera independientemente, y espero que Frege apoye los patrones de vista en el futuro. – dave4420

3

Inspirado por la solución de @ dbaupp Me gustaría añadir una versión de tipo fantasma para los candidatos:

-- using EmptyDataDecls 
data DayPrec 
data SecPrec 
data MilliPrec 
data NanoPrec 

data DateTime a = DateTime { days :: Int, sec :: Int, ms :: Int, ns :: Int } 

date :: Int -> DateTime DayPrec 
date d = DateTime d 0 0 0 

secDate :: Int -> Int -> DateTime SecPrec 
secDate d s = DateTime d s 0 0 

...  

--will work only for same precision which is a Good Thing (tm) 
instance Eq (DateTime a) where 
    (DateTime d s ms ns) == (DateTime d' s' ms' ns') = [d,s,ms,ns] == [d',s',ms',ns'] 

Si no me equivoco, esto me permite trabajar con un tipo, pero para distinguir las precisiones si es necesario. Pero supongo que también habrá algún inconveniente ...

+0

Me gusta esto, excepto que creo que esto podría hacer que algunas cosas sean un poco incómodas, p. Ej. calcular el tiempo entre un 'DateTime DayPrec' y' DateTime SecPrec' y obtener la precisión correcta (presumiblemente 'DayPrec'): creo que requeriría algo así como dependencias funcionales o familias de tipos, o escribir muchas declaraciones de instancia. (Suponiendo que tal operación es deseada y está bien definida.) – huon

+1

Creo que el usuario no debe mezclar diferentes precisiones, sino truncar o extender explícitamente la precisión antes de calcular. – Landei

+0

¿Cuál será el tipo de 'parseDateTime'? (Si el programador desea conservar el nivel de precisión en la cadena original, pero no sabe en tiempo de compilación, ¿cuánta precisión esperar?) – dave4420

0

Suponiendo que solo quiere resolver este problema (a diferencia de uno más genérico), entonces la biblioteca decimal podría ser lo que usted desea.