Qual è il vantaggio di separare i dati specializzati dal comportamento in un algoritmo?

0

La programmazione funzionale suggerisce strongmente di separare i dati dai comportamenti (funzioni). Tuttavia, non riesco a vedere il vantaggio di questo per un'implementazione di un algoritmo intrinsecamente legata a particolari dati delle impostazioni.

Ad esempio, supponiamo che esista un tratto LagrangeAlgorithmOOP con dati immutabili come le impostazioni algoritmiche, le specifiche del problema e le dipendenze dagli helper. I metodi del tratto usano tutti questi dati per trovare una soluzione al problema. La loro implementazione è specifica per il tipo dell'algoritmo. Quasi nessuno di questi avrebbe senso come funzione autonoma.

Specificamente, supponiamo di avere un refactoring

   
 trait LagrangeAlgorithOOP {

       val settings: SettingsLagrange
       val problem: ConstrainedProblem
       val innerMinimiser: Minimiser
       val penaltiesFunction: ConstraintPenaltiesFunction

       def iteration( s: StateLagrange):  Either[String, StateLagrange]
       def lagrangeFunction( lams: Lambdas, pens: Penalties): AugmentedLagrangianFunction
       def estimateLambdas( pens: Penalties, las: Lambdas, cons: ConstraintValues): Option[Lambdas]
       def updatePenaltiesHistory( h: HistoryLagrange): HistoryLagrange
     }   

in questo

case class LagrangeData(settings: SettingsLagrange,
                        problem: ConstrainedProblem,
                        innerMinimiser: Minimiser,
                        penaltiesFunction: ConstraintPenaltiesFunction)


trait LagrangeAlgorithmFUN {

  def iteration(d: LagrangeData, s: StateLagrange):  Either[String, StateLagrange]
  def lagrangeFunction(d: LagrangeData, lams: Lambdas, pens: Penalties): Lagrangian
  def estimateLambdas(d: LagrangeData, pens: Penalties, old: Lambdas, cons: ConstraintValues): Option[Lambdas]
  def updatePenaltiesHistory(d: LagrangeData,  h: HistoryLagrange): HistoryLagrange
}

Il secondo caso introduce una classe di dati e un parametro aggiuntivo in ciascun metodo. (Invece, potrei usare una monade Reader, che tuttavia richiederebbe anche trasformatori monad).

Domande:

  1. Qual è il miglior refactoring di questo algoritmo per uno stile FP?
  2. È possibile che un approccio a metà strada funzioni meglio: ad esempio, per lasciare alcuni dei campi dati nel tratto?
  3. C'è molta differenza tra le versioni originale e refactored?
  4. Qual è il vantaggio del refactoring in stile FP in questo caso?

Nota: sono d'accordo con molti punti nel post correlato Perché" accoppiamento stretto tra funzioni e dati "è errato? . Ancora non sono sicuro di come questo si applica ai dati delle impostazioni immutabili che sono intrinseci alle funzioni che implementano l'algoritmo.

    
posta Tupolev._ 16.10.2018 - 18:12
fonte

1 risposta

3

Se si specificano i dati del tipo come immutabili, allora c'è poca differenza tra i due esempi. Dici: "Il secondo caso introduce una classe di dati e un parametro aggiuntivo in ogni metodo." Ma in realtà no. Un metodo OO passa implicitamente il parametro che tu chiami un "parametro extra", quindi non è veramente extra, ma spostato prima dal nome della funzione a quello successivo.

Ricorda che senza ereditarietà la differenza tra foo.bar() e bar(foo) è puramente sintattica. Quando si aggiunge il fatto che la pratica moderna dell'OOP favorisce strongmente l'utilizzo dell'ereditarietà solo per le interfacce (che corrisponde strettamente alle classi di tipo FP) vedrete che c'è molta convergenza tra le migliori pratiche sia in OOP che in FP. Detto questo, vediamo ora che nei casi di "buone pratiche", foo.bar() e bar(foo) (o in un linguaggio ML (bar foo) ) è semplicemente una differenza sintattica che prende in considerazione anche le classi ereditarietà / tipo.

Una volta che permetti la mutabilità, allora una classe OO diventa effettivamente una monade IO specializzata e il programmatore OO finisce per dover lavorare con molte di queste monade IO specializzate per ottenere qualcosa. Anche in questo caso la tendenza attuale è quella di ridurre il numero di classi che sono esplicitamente modificabili, il che ha portato ad un'esplosione di linguaggi "in gran parte" ma non puramente funzionali, e con sintassi più nello stile OO che è familiare al programmatore tipico.

Quindi, in risposta alle tue domande, la differenza è in gran parte sintassi ma IMO lo stile OO di obj.func() consente una concatenazione più naturale delle chiamate di funzione. Invece di c(b(a)) , posso fare a.b().c() che rende l'ordine delle operazioni più chiaro perché legge nell'ordine da sinistra a destra.

L'argomento contatore è che lo stile di chiamata OO necessariamente dà la precedenza a un particolare parametro della funzione rispetto agli altri che è vero e una buona ragione per evitare lo stile se non c'è alcuna preferenza naturale dedotta dall'algoritmo. Ad esempio, quando confronti due oggetti per l'uguaglianza, a.equals(b) sembra semplicemente sciocco rispetto a equals(a, b) perché non c'è motivo di preferire a su b in quel contesto. Tutti i linguaggi OO che conosco, ad eccezione di Java, consentono funzioni "libere" che seguono lo stile più funzionale di non dare la precedenza a un particolare parametro. È persino fattibile in Java attraverso l'uso delle cosiddette funzioni "statiche" che esistono all'interno di una classe, ma in realtà non hanno un% co_de implicito o un parametro auto.

    
risposta data 21.10.2018 - 23:38
fonte