2012-09-05 8 views
7

I tienen un código que tiene una estructura equivalente a esto:¿Cómo puedo garantizar la eficacia cuando uso la aplicación parcial en Haskell puro?

import Debug.Trace 

newtype SomeExpensiveHiddenType = SCHT Double 

expensive :: Double -> Double -> SomeExpensiveHiddenType 
expensive a b = SCHT $ trace "call expensive" (*) a b 

cheap :: SomeExpensiveHiddenType -> Double -> Double 
cheap (SCHT x) c = trace "call cheap" (+) x c 

f1 :: Double -> Double -> Double -> Double 
f1 a b c = let x = expensive a b in cheap x c 

es decir f1 es una función que calcula un resultado caro en base a los dos primeros argumentos y, a continuación, utiliza este con el tercer argumento. Tenía la esperanza de que una aplicación parcial en los primeros 2 argumentos, luego la aplicación repetida del tercer argumento daría lugar a que el costoso cálculo solo se ejecutara una vez. Por desgracia, este no es el caso:

test1 = do 
    putStrLn "test 1" 
    let p = f1 2 3 
    print (p 0.1) 
    print (p 0.2) 
    print (p 0.3) 

resultados en:

*Main> test1 
test 1 
call cheap 
call expensive 
6.1 
call cheap 
call expensive 
6.2 
call cheap 
call expensive 
6.3 
*Main> 

Yo he llegado con lo que parece ser una solución:

newtype X a = X { unX :: a } 
f2 :: Double -> Double -> X (Double -> Double) 
f2 a b = let x = expensive a b in X (cheap x) 

test2 = do 
    putStrLn "test 2" 
    let p = unX $ f2 2 3 
    print (p 0.1) 
    print (p 0.2) 
    print (p 0.3) 

lo que resulta en:

*Main> test2 
test 2 
call cheap 
call expensive 
6.1 
call cheap 
6.2 
call cheap 
6.3 
*Main> 

Pero esto parece bastante complicado . ¿Hay alguna manera más limpia de evitar las llamadas redundantes al costoso calc?

+3

Además, no pruebe tales cosas en ghci, compile con optimizaciones. En casos simples como este, GHC compartirá 'p' incluso con la definición original de' f1'. (Pero debe usar el consejo de dbaupp, sin embargo, en situaciones más complicadas, el compilador puede no ser capaz de introducir el intercambio.) –

+0

Utilicé ghci de forma consciente, ya que confiar en las optimizaciones parece peligroso. El tiempo de ejecución del programa real diferirá en un orden de magnitud si no se descubre el intercambio. – timbod

+0

Correcto, no deberías (ciegamente) confiar en las optimizaciones. Por eso dije que deberías seguir los consejos de dbaupp. Sin embargo, si desea saber cómo se comportará su código, no hay sustituto para probar realmente su código. El código que está probando en ghci es diferente, puede tener características completamente diferentes que el código real. GHCi es ideal para probar la corrección, pero no para la velocidad ni el uso de la memoria. –

Respuesta

9

Puede simplemente colocar el tercer argumento dentro de let, de modo que x se comparta.

f2 a b = let x = expensive a b in \c -> cheap x c 

(En este caso f2 a b = let x = expensive a b in cheap x también funciona.)


Lo que se busca es impulsada por el compilador partial evaluation, y que es un problema difícil ... al menos es lo suficientemente duro para implementar correctamente que no está en GHC.

+0

(¡Eso fue rápido!) Confirmo que de hecho resuelve mi problema. Hasta ahora, hubiera considerado mi f1 y tu f2 equivalentes. Tendré que tener en cuenta esta transformación en el futuro cada vez que escribo funciones destinadas a ser aplicadas parcialmente. – timbod

+0

@timbod su 'f1' es' \ a -> \ b -> \ c-> let x = expens ab en cheap xc', y el 'f2' aquí es' \ a -> \ b-> let x = gasto ab en \ c-> barato xc'. Que por * eta-reduction * es '\ a -> \ b-> let x = expens a b en x barato (aquí también). Que es exactamente equivalente a * su 'f2' *, :) no hay ningún boxeo involucrado porque' newtype' se elimina en tiempo de compilación. 'newtype' se usa generalmente para permitir la implementación alternativa de 'instancia' de alguna clase de letra o algún truco de tipo. Para * flotar * que * let * 'x = expens a b' arriba' \ c-> '* lambda * es un movimiento audaz, difícil 4 _a compiler_ para decidir si vale la pena o no. –

Cuestiones relacionadas