programmazione funzionale: impatto dei tipi di dati typedefing sulla leggibilità e manutenzione del codice

3

Nei linguaggi di programmazione funzionale, come Scala, i tipi di dati e le strutture, sono davvero importanti. Sono in due a proposito dell'uso di type-defs nell'aiutare la leggibilità del codice a manipolare strutture di dati non banali.

Ecco un esempio di una funzione che accetta una raccolta generica in Scala, la attraversa una volta in parallelo e calcola il suo valore medio. Qui ho usato un type-def semplicemente per non avere (Int,Int) dappertutto:

def average(xs:GenTraversable[Int]):Int={
        type IntTuple = (Int,Int)
        def addIntTuples(x:IntTuple,y:IntTuple):IntTuple=(x._1+y._1,x._2+y._2)
        val (sum,len)=xs.map(x=>(x,1))
            .aggregate((0,0))(addIntTuples,addIntTuples)
        sum/len
    }

Ecco un'altra versione della funzione di cui sopra che tenta di dare al lettore un'idea migliore di ciò che la funzione sta facendo introducendo typedefs che indica cosa rappresentano i valori nella tupla.

def readableAverage(xs:GenTraversable[Int]):Int={
        type Sum = Int
        type Len = Int
        type SumLen = (Sum,Len)
        def add(x:SumLen,y:SumLen):SumLen=(x._1+y._1,x._2+y._2)
        val (sum,len)=xs.map(x=>(x,1))
            .aggregate((0,0))(add,add)
        sum/len
    }

La seconda versione è più lunga, ma forse offre al lettore più informazioni su come funziona la funzione. La domanda è: in primo luogo, consideri la seconda versione effettivamente più leggibile e perspicace? Se è così, è il vantaggio aggiunto vale la pena aumentare la lunghezza del codice ?

    
posta user4376555 23.03.2017 - 17:43
fonte

2 risposte

6

Preferisco strongmente la prima versione: addIntTuples fa esattamente quello che dice. È un metodo generico che potrebbe persino esistere al di fuori di questo ambito. Ciò significa che quando ragiono sul codice, posso pensare:

okay this function just adds pairs of Ints, simple, lets see what the rest does...

L'altra versione impone un significato specifico, che devo apprezzare prima di osservare come viene effettivamente utilizzata. Quindi devo tornare e controllare:

What is this SumLen again? Ah.. just a tuple of these Sum and Len... What type was Sum again? Int or Double? Int, (why?) Okay, lets go back again...

Questo è ovviamente esagerato per piccole funzioni, ma puoi vedere che può diventare un problema per quelle più grandi. Generalmente trovo gli alias di tipo che oscurano il tipo sottostante fastidioso.

Quando due approcci sembrano di complessità simile, scelgo sempre quello più generico. Per esempio. prova a separare l'essenza di ciò che un metodo fa da metodi di utilità. Ciò significa che puoi facilmente calcolare un'utilità di uso comune e IMHO rende il codice più facile da ragionare.

EDIT:

Il vantaggio principale di avere metodi helper / util generici è che comunichi che non c'è "niente da vedere qui", nessuna logica aziendale complicata, solo qualcosa che volevi nascondere / astrarre dalle parti interessanti del codice.

Controlla questo risposta SO pertinente che utilizza semigruppo scalaz :

import scalaz._, Scalaz._

scala> (1, 2.5) |+| (3, 4.4)
res0: (Int, Double) = (4,6.9)

o la seconda risposta che usa Numeric per creare essenzialmente la stessa cosa che fornisce scalaz:

implicit class Tupple2Add[A : Numeric, B : Numeric](t: (A, B)) {
  import Numeric.Implicits._

  def |+| (p: (A, B)) = (p._1 + t._1, p._2 + t._2)
}

(2.0, 1) |+| (1.0, 2) == (3.0, 3)

Questi non solo creano codice riutilizzabile, ma fanno qualcosa di più importante: comunicano che non c'è niente di speciale lì. Per esempio. non c'è niente di speciale su Int , funziona con qualsiasi tipo che abbia un Numeric , in modo che possa aggiungerli p._1 + t._1 .

C'è un bel discorso che tocca questo argomento, Vincoli Liberare, Liberties Vincolare - Runar Bjarnason In a poche parole:

def f[T](a:T):T ha una sola implementazione valida: def f[T](a:T):T = a . Essendo così generico, il metodo è vincolato a una singola implementazione valida. def f(a:Int):Int ha un'implementazione valida Int.MaxValue * 2 .

Il messaggio da asporto è che lasciare il codice inutilmente specifico per un particolare caso d'uso lo apre a molteplici (e forse errate) implementazioni e interpretazioni mentali.

Per quanto riguarda i tipi alias, non mi piacciono molto perché danno semplicemente un nome diverso allo stesso tipo, e il compilatore accetterà volentieri entrambi. Mi piacciono di più classi di valore e tag tagged link . Entrambi creano un tipo diverso dall'originale, ad es. Int , quindi il compilatore si lamenterà se usi e.g. un tipo Len nel punto in cui si aspetta un tipo Sum .

    
risposta data 10.04.2017 - 15:34
fonte
5

Se avessi intenzione di raggiungere questo tipo di lunghezze per migliorare la leggibilità del mio codice, vorrei solo creare una classe SumLen

case class SumLen(sum: Int, len: Int) {
  def add(that: SumLen): SumLen = {SumLen(sum + that.sum, len + that.len)}
}

Con le modifiche alla sintassi del codice rimanente.

Sono sicuro che i typedef sono perfetti per alcune situazioni, ma in questo caso non mi sembra che tirino il loro peso.

    
risposta data 23.03.2017 - 18:19
fonte

Leggi altre domande sui tag