"Let" di Clojure Equivalente in Scala
Domanda
Spesso affronto la seguente situazione: supponiamo di avere queste tre funzioni
def firstFn: Int = ...
def secondFn(b: Int): Long = ...
def thirdFn(x: Int, y: Long, z: Long): Long = ...
E anche io ho calculate
funzione. Il mio primo approccio può apparire così:
def calculate(a: Long) = thirdFn(firstFn, secondFn(firstFn), secondFn(firstFn) + a)
Sembra bellissimo e senza parentesi ricci - solo un'espressione. Ma non è ottimale, quindi finisco con questo codice:
def calculate(a: Long) = {
val first = firstFn
val second = secondFn(first)
thirdFn(first, second, second + a)
}
Ora sono diverse espressioni circondate da parentesi ricci. In tali momenti invidio un po 'Clojure. Insieme a Lascia la funzione Posso definire questa funzione in un'unica espressione.
Quindi il mio obiettivo qui è definire calculate
funzione con un'espressione. Mi viene in mente 2 soluzioni.
1 - con Scalaz Posso definirlo in questo modo (ci sono modi migliori per farlo con Seraz?):
def calculate(a: Long) =
firstFn |> {first => secondFn(first) |> {second => thirdFn(first, second, second + a)}}
Quello che non mi piace di questa soluzione è che è nidificato. Più val
S ho più profondo che questo nidificazione è.
2 - con for
Comprensione posso ottenere qualcosa di simile:
def calculate(a: Long) =
for (first <- Option(firstFn); second <- Option(secondFn(first))) yield thirdFn(first, second, second + a)
Da una mano questa soluzione ha una struttura piatta, proprio come let
In Clojure, ma dall'altra parte ho bisogno di avvolgere le funzioni. Option
e ricevere Option
come risultato da calculate
(Va bene, ho a che fare con Nulls, ma non ... e non voglio).
Ci sono modi migliori per raggiungere il mio obiettivo? Qual è il modo idiomatico per affrontare tali situazioni (potrei essere con cui dovrei restare con val
s ... ma let
Il modo di farlo sembra così elegante)?
D'altra parte è collegato a Trasparenza referenziale. Tutte e tre le funzioni sono referenzialmente trasparenti (nel mio esempio firstFn
Calcola qualche costante come PI), quindi teoricamente possono essere sostituiti con i risultati di calcolo. Lo so, ma il compilatore no, quindi non può ottimizzare il mio primo tentativo. Ed ecco la mia seconda domanda:
Posso in qualche modo (posso essere con l'annotazione) dare un suggerimento al compilatore, che la mia funzione è referenzialmente trasparente, in modo che possa ottimizzare questa funzione per me (ad esempio mettere una sorta di memorizzazione nella cache)?
Modificare
Grazie a tutti per le fantastiche risposte! È semplicemente impossibile selezionare una risposta migliore (può essere perché sono tutti così buoni), quindi accetterò la risposta con i voti più aggiornati, penso che sia abbastanza giusto.
Soluzione
Nel caso non ricorsivo, let è una ristrutturazione di lambda.
def firstFn : Int = 42
def secondFn(b : Int) : Long = 42
def thirdFn(x : Int, y : Long, z : Long) : Long = x + y + z
def let[A, B](x : A)(f : A => B) : B = f(x)
def calculate(a: Long) = let(firstFn){first => let(secondFn(first)){second => thirdFn(first, second, second + a)}}
Certo, è ancora nidificato. Non posso evitarlo. Ma hai detto che ti piace la forma monadica. Quindi ecco la monade dell'identità
case class Identity[A](x : A) {
def map[B](f : A => B) = Identity(f(x))
def flatMap[B](f : A => Identity[B]) = f(x)
}
Ed ecco il tuo calcolo monadico. Scartare il risultato chiamando .x
def calculateMonad(a : Long) = for {
first <- Identity(firstFn)
second <- Identity(secondFn(first))
} yield thirdFn(first, second, second + a)
Ma a questo punto sembra sicuramente la versione Val originale.
L'identità Monad esiste in Scalaz con più raffinatezza
http://scalaz.googlecode.com/svn/continuou/latest/browse.sxr/scalaz/idenza.scala.html
Altri suggerimenti
Resta con la forma originale:
def calculate(a: Long) = {
val first = firstFn
val second = secondFn(first)
thirdFn(first, second, second + a)
}
È conciso e chiaro, anche per gli sviluppatori di Java. È approssimativamente equivalente a lasciare, proprio senza limitare l'ambito dei nomi.
Ecco un'opzione che potresti aver trascurato.
def calculate(a: Long)(i: Int = firstFn)(j: Long = secondFn(i)) = thirdFn(i,j,j+a)
Se vuoi effettivamente creare un metodo, questo è il modo in cui lo farei.
In alternativa, potresti creare un metodo (si potrebbe nominarlo let
) che evita di nidificare:
class Usable[A](a: A) {
def use[B](f: A=>B) = f(a)
def reuse[B,C](f: A=>B)(g: (A,B)=>C) = g(a,f(a))
// Could add more
}
implicit def use_anything[A](a: A) = new Usable(a)
def calculate(a: Long) =
firstFn.reuse(secondFn)((first, second) => thirdFn(first,second,second+a))
Ma ora potresti dover nominare le stesse cose più volte.
Se ritieni che la prima forma sia più pulita/più elegante/più leggibile, allora perché non attenersi?
Innanzitutto, leggi Questo recente messaggio di commit al compilatore Scala da nientemeno che Martin Odersky e prendilo a cuore ...
Forse il vero problema qui è saltare immediatamente la pistola sostenendo che non è ottimale. Il JVM è piuttosto caldo nell'ottimizzare questo genere di cose. A volte è semplicemente fantastico!
Supponendo che tu abbia un problema di prestazioni autentico in un'applicazione che ha una vera necessità di una velocità, dovresti iniziare con un rapporto Profiler dimostrazione Che si tratta di un collo di bottiglia significativo, su un JVM adeguatamente configurato e riscaldato.
Quindi, e solo allora, dovresti guardare modi per renderlo più veloce che potrebbe finire per sacrificare la chiarezza del codice.
Perché non usare la corrispondenza dei pattern qui:
def calcola (a: long) = firstfn match {case f => secondfn (f) match {case s => terzofn (f, s, s + a)}}
Che ne dici di usare Currying per registrare i valori di restituzione della funzione (i parametri dei gruppi di parametri precedenti sono disponibili in gruppi di superamento).
Un po 'strano ma abbastanza conciso e nessuna ripetuta invocazioni:
def calculate(a: Long)(f: Int = firstFn)(s: Long = secondFn(f)) = thirdFn(f, s, s + a)
println(calculate(1L)()())