L'équivalent 'let' de Clojure en Scala
Question
Je suis souvent confronté à la situation suivante :supposons que j'ai ces trois fonctions
def firstFn: Int = ...
def secondFn(b: Int): Long = ...
def thirdFn(x: Int, y: Long, z: Long): Long = ...
et j'ai aussi calculate
fonction.Ma première approche peut ressembler à ceci :
def calculate(a: Long) = thirdFn(firstFn, secondFn(firstFn), secondFn(firstFn) + a)
Il est magnifique et sans accolades - juste une expression.Mais ce n'est pas optimal, donc je me retrouve avec ce code :
def calculate(a: Long) = {
val first = firstFn
val second = secondFn(first)
thirdFn(first, second, second + a)
}
Il s'agit maintenant de plusieurs expressions entourées d'accolades.Dans de tels moments, j’envie un peu Clojure.Avec laisser fonctionner Je peux définir cette fonction en une seule expression.
Mon objectif ici est donc de définir calculate
fonction avec une expression.Je propose 2 solutions.
1 - Avec Scalaz Je peux le définir comme ceci (existe-t-il de meilleures façons de le faire avec Scalaz ?) :
def calculate(a: Long) =
firstFn |> {first => secondFn(first) |> {second => thirdFn(first, second, second + a)}}
Ce que je n'aime pas dans cette solution, c'est qu'elle est imbriquée.Le plus val
s J'ai plus cette nidification est profonde.
2 - Avec for
compréhension, je peux réaliser quelque chose de similaire :
def calculate(a: Long) =
for (first <- Option(firstFn); second <- Option(secondFn(first))) yield thirdFn(first, second, second + a)
D'une part, cette solution a une structure plate, tout comme let
dans Clojure, mais d'un autre côté, je dois envelopper les résultats des fonctions dans Option
et recevoir Option
comme résultat de calculate
(c'est bien que j'aie affaire à des valeurs nulles, mais je ne le fais pas...et je ne veux pas).
Existe-t-il de meilleures façons d’atteindre mon objectif ?Quelle est la manière idiomatique de gérer de telles situations (peut-être devrais-je m'en tenir à val
s...mais let
la façon de le faire a l'air si élégante) ?
D'un autre côté, il est connecté à Transparence référentielle.Les trois fonctions sont référentiellement transparentes (dans mon exemple firstFn
calcule certaines constantes comme Pi), donc théoriquement elles peuvent être remplacées par des résultats de calcul.Je le sais, mais pas le compilateur, il ne peut donc pas optimiser ma première tentative.Et voici ma deuxième question :
Puis-je d'une manière ou d'une autre (peut-être avec une annotation) donner une indication au compilateur que ma fonction est référentiellement transparente, afin qu'elle puisse optimiser cette fonction pour moi (y mettre une sorte de mise en cache, par exemple) ?
Modifier
Merci à tous pour les excellentes réponses !Il est tout simplement impossible de sélectionner la meilleure réponse (peut-être parce qu'elles sont toutes si bonnes), donc j'accepterai la réponse avec le plus de votes positifs, je pense que c'est assez juste.
La solution
dans le cas non récursif, soit une restructuration de 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)}}
Bien sûr, c'est toujours imbriqué.Je ne peux pas éviter ça.Mais vous avez dit que vous aimiez la forme monadique.Voici donc la monade identitaire
case class Identity[A](x : A) {
def map[B](f : A => B) = Identity(f(x))
def flatMap[B](f : A => Identity[B]) = f(x)
}
Et voici votre calcul monadique.Déballez le résultat en appelant .x
def calculateMonad(a : Long) = for {
first <- Identity(firstFn)
second <- Identity(secondFn(first))
} yield thirdFn(first, second, second + a)
Mais à ce stade, cela ressemble certainement à la version val originale.
La monade Identité existe dans Scalaz avec plus de sophistication
http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/Identity.scala.html
Autres conseils
Restez fidèle au formulaire original :
def calculate(a: Long) = {
val first = firstFn
val second = secondFn(first)
thirdFn(first, second, second + a)
}
C'est concis et clair, même pour les développeurs Java.C'est à peu près équivalent à laisser, sans limiter la portée des noms.
Voici une option que vous avez peut-être négligée.
def calculate(a: Long)(i: Int = firstFn)(j: Long = secondFn(i)) = thirdFn(i,j,j+a)
Si vous voulez réellement créer une méthode, c’est ainsi que je procéderais.
Alternativement, vous pouvez créer une méthode (on pourrait la nommer let
) qui évite l'imbrication :
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))
Mais maintenant, vous devrez peut-être nommer les mêmes choses plusieurs fois.
Si vous pensez que le premier formulaire est plus propre/plus élégant/plus lisible, alors pourquoi ne pas simplement vous y tenir ?
Tout d’abord, lisez ce récent message de commit au compilateur Scala de nul autre que Martin Odersky et prenez-le à cœur...
Peut-être que le vrai problème ici est de sauter immédiatement le pas en affirmant que ce n'est pas optimal.La JVM est très efficace pour optimiser ce genre de choses.Parfois, c'est tout simplement incroyable !
En supposant que vous rencontriez un véritable problème de performances dans une application qui a réellement besoin d'une accélération, vous devriez commencer par un rapport de profileur. prouver qu'il s'agit d'un goulot d'étranglement important, sur une JVM correctement configurée et réchauffée.
Alors, et alors seulement, devriez-vous chercher des moyens de le rendre plus rapide, ce qui pourrait finir par sacrifier la clarté du code.
Pourquoi ne pas utiliser la correspondance de modèles ici :
def calculer (a :Long) = firstFn match { case f => secondFn(f) match { case s => troisièmeFn(f,s,s + a) } }
Que diriez-vous d'utiliser le currying pour enregistrer les valeurs de retour de la fonction (les paramètres des groupes de paramètres précédents sont disponibles dans les groupes suivants).
Un peu bizarre mais assez concis et sans invocations répétées :
def calculate(a: Long)(f: Int = firstFn)(s: Long = secondFn(f)) = thirdFn(f, s, s + a)
println(calculate(1L)()())