Pillole di Haskell - un uso di id

By abell on 2008-12-10-22:40:11 | In haskell programmazione funzionale

La funzione id (per identity=identità) in Haskell prende il suo argomento e lo restituisce immutato. La sua definizione è

id :: a -> a
id x = x
id è quindi l'elemento neutro per composizione di funzioni, cioè per ogni f :: a -> b, si ha che
f = f . id = id . f

id non sembra di grande utilità in quanto, di fatto, non fa niente. La sua applicazione a un argomento si può sempre omettere, così come la sua composizione con un'altra funzione. L'unico uso possibile sembrerebbe quello di essere passata come argomento a una funzione di ordine superiore che si aspetti una generica funzione a->a. Chiaramente, nei soli casi in cui passare id abbia senso nella logica del programma.

...MA...

se pure id non ha effetti dal punto di vista computazionale, possiamo utilizzarla per avere vantaggi in fase di compilazione.

Se definiamo una nuova funzione

myid :: A -> A
myid = id

dove A è uno specifico tipo di dati (un ADT o un type, anche parzialmente specificati), allora myid forza il tipo dell'argomento, anche nel caso in cui questo sia a priori generico.

Ad esempio, poniamo di avere in una libreria un ADT abbastanza complesso:

data ComplexData a b c d = FirstCD a | SecondCD b c
                         | ThirdCD a c | FourthCD d

I parametri di tipo a, b, c e d sono completamente generici, ma mettiamo di dover lavorare spesso con ComplexData in cui b sia di tipo Int. Possiamo usare id con una restrizione sul tipo (dandole un'altro nome), come segue:

forceSecondInt :: ComplexData a Int c d -> ComplexData a Int c d
forceSecondInt = id
Con questa nuova id, possiamo restringere qualsiasi funzione funzioneStrana che restituisce un ComplexData al solo caso in cui il secondo parametro del risultato sia di tipo Int, usando
forceSecondInt . funzioneStrana

Un altro uso di questo trucchetto è per aggirare la Monomorphysm Restriction, che a volte può costringerci a definire esplicitamente il tipo di una funzione. Nel caso in cui il tipo sia molto complesso, la definizione può diventare difficile da gestire. Può accadere ad esempio che il compilatore si lamenti (a causa della Monomorphysm Restriction) per una certa definizione

megaFunzione a b c d e = <corpo della megaFunzione ...>
e richieda di specificarne il tipo. Questo può essere molto complicato, come ad esempio
megaFunzione :: ( Show a, MyClass n, Monad m ) =>
                ( a -> ( n, a ) -> Int ) -> ( m String )
             -> ( ( n -> Int ) -> m n ) -> n
             -> ( ( a, n ) -> ( m a, m n ) )
             -> ( n, n )
ma può darsi che tutti gli elementi della definizione siano derivati dal corpo della funzione, tranne il secondo che dobbiamo specificare come m String. In questo caso, un'alternativa a
megaFunzione :: ( Show a, MyClass n, Monad m ) =>
                ( a -> ( n, a ) -> Int ) -> ( m String )
             -> ( ( n -> Int ) -> m n ) -> n
             -> ( ( a, n ) -> ( m a, m n ) )
             -> ( n, n )
megaFunzione a b c d e = <corpo della megaFunzione ...>
è
myId :: ( Monad m ) => ( a -> m String -> c ) -> ( a -> m String -> c )
myId = id

megaFunzione a b c d e = myId $ <corpo della megaFunzione ...>
myId crea un vincolo in più per l'inferenza dei tipi in megaFunzione, quel tanto che basta per derivare il tipo complesso che avremmo dovuto specificare per esteso.