40 votos

¿Cómo experimentaron Haskell desarrolladores enfoque de la pereza en *diseño* tiempo?

Soy un intermedio de Haskell programador con toneladas de experiencia en estricto FP y no-FP idiomas. La mayoría de mis Haskell código analiza moderadamente grandes conjuntos de datos (10^6..10^9 cosas), así que la pereza siempre está al acecho. Tengo una razonablemente buena comprensión de los procesadores, WHNF, la coincidencia de patrones, y compartir, y he sido capaz de arreglar las fugas con la explosión de los patrones y seq, pero este perfil y orar enfoque se siente sórdido y lo malo.

Quiero saber cómo experimentado Haskell programadores enfoque de la pereza en tiempo de diseño. No estoy preguntando acerca de fácil como elementos de Datos.ByteString.Perezoso o foldl'; más bien, quiero saber la forma de pensar en el nivel inferior perezoso maquinaria que causa problemas de memoria en tiempo de ejecución y difícil de depurar.

¿Qué piensa usted acerca de los procesadores, la coincidencia de patrones, y compartir durante el tiempo de diseño?

¿Qué patrones de diseño y expresiones idiomáticas que utilizan para evitar fugas?

¿Cómo aprender estos patrones y frases hechas, y usted tiene algunas buenas referencias?

¿Cómo se puede evitar la prematura optimización de no fugas no de problemas?

(Modificado 2014-05-15 por el tiempo de elaboración de presupuestos):

¿Presupuesto sustancial de tiempo del proyecto para encontrar y solucionar problemas de memoria?

O, hacer tus habilidades de diseño suelen eludir los problemas de la memoria, y se obtiene la espera de consumo de memoria muy temprano en el ciclo de desarrollo?

45voto

luqui Puntos 26009

Creo que la mayoría de los problemas con "rigor fugas" sucede porque la gente no tiene un buen modelo conceptual. Haskellers sin un buen modelo conceptual que tienden a propagar la superstición de que más estrictas es mejor. Tal vez esta intuición proviene de sus resultados de jugando con pequeños ejemplos y estrecha lazos. Pero es incorrecto. Es tan importante como ser perezoso en el momento preciso como para ser estrictos en el momento correcto.

Hay dos campos de tipos de datos, generalmente se conoce como "datos" y "codata". Es esencial respetar los patrones de cada uno.

  • Operaciones que producen los "datos" (Int, ByteString, ...) deben ser obligados cerca de donde se producen. Si puedo añadir un número a un acumulador, soy muy cuidadoso para asegurarse de que se vea obligado antes de agregar otro. Una buena comprensión de la pereza es muy importante aquí, especialmente, por su naturaleza condicional (es decir, la rigurosidad de las proposiciones no tomar la forma "X es evaluada", sino "cuando Y es evaluado, por lo que es X").
  • Operaciones que producen y consumen "codata" (listas de la mayoría de las veces, los árboles, la mayoría de los otros tipos recursivos) deben hacerlo de forma gradual. Generalmente codata -> codata transformación debe producir algo de información para cada bit de información que consumen (modulo saltando como filter). Otra pieza importante para codata es que se usa de manera lineal, siempre que sea posible, es decir, el uso de la cola de una lista de exactamente una vez; el uso de cada rama en el árbol exactamente una vez. Esto asegura que la GC puede recoger las piezas que se consumen.

Las cosas toman una cantidad especial de atención cuando usted tiene codata que contiene los datos. E. g. iterate (+1) 0 !! 1000 terminará produciendo un tamaño de 1000 procesador antes de evaluar. Usted necesita pensar acerca de condicional rigor de nuevo, la forma de prevenir este caso es asegurar que cuando un contras de la lista que se consume, la adición de su elemento se produce. iterate viole esta, por lo que necesitamos una mejor versión.

iterate' :: (a -> a) -> a -> [a]
iterate' f x = x : (x `seq` iterate' f (f x))

Como empezar a componer las cosas, por supuesto, se hace más difícil decir cuando el mal casos sucede. En general es difícil hacer eficiente de estructuras de datos y funciones que funcionan igual de bien en los datos y en codata, y es importante tener en cuenta cual es cual (incluso en un polimórficos de la creación, donde no está garantizada, usted debe tener uno en mente y tratar de respeto).

Compartir es difícil, y creo que el enfoque es principalmente en un caso-por-caso. Porque es difícil, trato de mantener la localizada, la elección de no exponer a grandes estructuras de datos para el módulo de usuarios en general. Esto se puede hacer mediante la exposición de combinadores para la generación de la cosa en cuestión y, a continuación, producir y consumir todo de una sola vez (la codensity transformación de las mónadas es un ejemplo de esto).

Mi objetivo es conseguir que cada función sea respetuoso de los datos / codata patrones de mi tipo. Por lo general se puede golpear (aunque a veces se requiere de mucho pensar, se ha convertido en natural lo largo de los años), y yo rara vez tienen problemas de fugas cuando lo hago. Pero no estoy diciendo que es fácil, se requiere experiencia con la canónica de bibliotecas y de los patrones de la lengua. Estas decisiones no se toman de forma aislada, y todo tiene que estar a la vez para que funcione bien. Un mal afinado instrumento puede arruinar todo el concierto (que es por eso que "la optimización de la perturbación aleatoria" casi nunca funciona para este tipo de problemas).

Apfelmus del Espacio Invariantes artículo es útil para el desarrollo de su espacio/procesador la intuición más. También ver a Edward Kmett del comentario de abajo.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X