2012-09-05 14 views
7

mam kod, który ma strukturę podobną do odpowiadającej poniżej:Jak zapewnić efektywność podczas stosowania częściowego zastosowania w czystym Haskell?

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 

f1 jest to funkcja, która oblicza drogi wyniku na podstawie pierwszych dwóch argumentów, a następnie za pomocą tego z trzecim argumentem. Miałem nadzieję, że częściowa aplikacja na pierwszych 2 argumentach, a następnie powtórzone zastosowanie trzeciego argumentu spowoduje, że kosztowne obliczenia będą wykonywane tylko raz. Niestety nie jest to przypadek:

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

skutkuje:

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

mam wymyślić, co wydaje się być rozwiązaniem:

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) 

skutkuje:

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

Ale wydaje się to dość niechlujne . Czy istnieje lepszy sposób na uniknięcie zbędnych połączeń z drogim kalkulatorem?

+3

Ponadto, nie testuj takich rzeczy w ghci, kompiluj z optymalizacją. W prostych przypadkach, takich jak ten, GHC będzie wtedy udostępniać 'p' nawet z oryginalną definicją' f1'. (Ale powinieneś skorzystać z porady dbaupp, jednak w bardziej skomplikowanych sytuacjach kompilator może nie być w stanie przedstawić samego udostępniania.) –

+0

Świadomie użyłem tutaj ghci, ponieważ poleganie na optymalizacjach wydaje się niebezpieczne. Środowisko wykonawcze programu rzeczywistego różni się o rząd wielkości, jeśli udostępnianie nie zostanie wykryte. – timbod

+0

Dobrze, nie powinieneś (na ślepo) polegać na optymalizacjach. Dlatego powiedziałem, że powinieneś przestrzegać porady Dbauppa. Niemniej jednak, jeśli chcesz się dowiedzieć, jak zachowa się twój kod, nie ma żadnego substytutu dla testowania twojego kodu. Kod, który testujesz w ghci, jest inny, może mieć zupełnie inną charakterystykę niż prawdziwy kod. GHCi doskonale nadaje się do testowania poprawności, ale nie do prędkości ani użycia pamięci. –

Odpowiedz

9

Możesz umieścić trzeci argument wewnątrz let, aby udostępnić x.

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

(W tym przypadku f2 a b = let x = expensive a b in cheap x działa zbyt.)


Co szukasz jest napędzany partial evaluation kompilator, i to jest problem trudny ... przynajmniej jest wystarczająco trudne do poprawnie wdrożyć, że nie jest w GHC.

+0

(To było szybkie!) Potwierdzam, że to rzeczywiście rozwiązuje mój problem. Do tej pory uważałbym, że moje f1 i twoje f2 są równoważne. Będę potrzebował zachować tę transformację w przyszłości, kiedy piszę funkcje, które mają być częściowo zastosowane. – timbod

+0

@timbod twoje 'f1' jest' \ a -> \ b -> \ c-> let x = kosztuje ab w tanim xc', a 'f2' tutaj jest' \ a -> \ b-> let x = expens ab in \ c-> cheap xc'. Które przez * redukcja eta * wynosi '\ a -> \ b-> let x = kosztuje b w tanim x' (również tutaj). Które jest dokładnie równoważne * twojemu 'f2' *, :) nie ma nawet żadnego boksu, ponieważ' newtype' jest eliminowany podczas kompilacji. 'newtype' jest zwykle używany, aby umożliwić alternatywną implementację" instancji "jakiejś typeclass lub jakiejś sztuczki typu. To * float * that * let * 'x = expens b' above' \ c-> '* lambda * to odważny ruch, trudny 4 _a kompilator_, który decyduje, czy jest tego wart, czy nie. –

Powiązane problemy