Quelle est la manière idiomatique FP pour cela: disons que j'ai ceci
def split = name.splitAt(" ") //some more functions
Maintenant, j'ai quelques fonctions d'aide telles que
trait Name object Name{ def apply(name: String): Name = { if (name.trim.isEmpty || name.trim.length < 3) InvalidName else ValidName(name.trim) } } case object InvalidName extends Name case class ValidName(name:String) extends AnyVal with Name
quel chemin est le plus idiomatique:
Mettez-les dans la classe de cas elle-même, mais cela fait que la classe de cas contient une logique, mais je peux faire quelque chose comme:
val n = ValidName ("john smith")
val (premier, dernier) = n.split
Mettez-les dans un objet dédié mais alors la méthode ressemblera à
def split (n: ValidName) = n.name.splitAt ("")
Créez un objet avec une classe implicite qui acceptera Name et appellera les méthodes
Que pensez-vous?
3 Réponses :
Il n'y a pas de problème majeur avec l'ajout de logique à une classe de cas
, en particulier lorsqu'il s'agit simplement d'extraire les données dans un format différent. (Cela devient problématique lorsque vous ajoutez des membres de données à une classe de cas
).
Je ferais donc l'option 1 sans m'en soucier!
En réponse aux commentaires, classe de cas
n'est en fait qu'un raccourci pour créer une classe avec tout un tas de méthodes pré-implémentées utiles. En particulier, la méthode unapply
permet d'utiliser une classe de cas
dans la mise en correspondance de modèles, et est égal à
effectue une comparaison élément par élément des champs de deux instances.
Et il existe un tas d'autres méthodes pour extraire les données de la classe de cas
de différentes manières. Les plus évidents sont toString
et copy
, mais il y en a d'autres comme hashCode
et tout ce qui est hérité de Product
, comme productIterator
.
Puisqu'une classe de cas
a déjà des méthodes pour extraire les données de manière utile, je ne vois aucune objection à ajouter votre méthode split
comme une autre façon d'extraire des données de < code> classe de cas .
Les classes de cas ne sont pas supposées contenir la fonction pour les données, elles sont utilisées pour définir la structure algébrique et principalement utilisées pour la correspondance de modèles.
en fait, tout ce tas de méthodes pré-implémentées est hérité par produit de trait, il ne les a pas. Et c’est ce que j’ai dit que la fonction sur les données devrait rester à l’extérieur dans une autre entité comme un trait ou un objet, etc.
Merci Tim, je suis en quelque sorte entre votre suggestion et mon opinion qui correspond à ce que @RamanMishra a dit concernant la classe de cas en tant que type de données algébrique. Je veux généralement laisser la logique biz en dehors de la classe de cas. Je peux bien sûr créer un objet qui contiendra cette logique comme option 3 mais il semble être une surcharge redondante wdyt?
@igx Je suis d'accord que la logique métier ne doit pas être dans une classe de cas
, mais je pense que les opérations simples sont OK tant qu'elles se rapportent directement aux valeurs de la classe elle-même. Il est normal pour un ADT d'avoir d'autres méthodes qui les décrochent, ainsi que la correspondance de modèle.
Je pense que cela dépend du contexte. Dans ce cas, si la plupart des méthodes que vous utilisez ne sont que de légères modifications apportées aux méthodes String
, vous pouvez envisager une quatrième option:
case class Name(name: String) implicit def NameToString(n: Name) = n.name Name("Iron Man").split(" ") // Array(Iron, Man)
Je vois l'avantage de votre suggestion, mais en ce qui concerne la décision de conception: vous ne voulez pas remplir votre code d'implicites. vous souhaitez utiliser implicits (IMHO) où vous souhaitez étendre une bibliothèque externe pour s'adapter à votre dsl ou pratique. ou pour la conversion nécessaire. dans votre exemple, vous convertissez simplement Name
en String
, ce qui est même dangereux, par exemple, vous pouvez effectuer une opération telle que `` `val name = Name (" John Doe ") name + "quelque chose" `` `c'est un comportement inattendu
Plus idiomatique:
case class Name (first: String, last: String) { lazy val fullName = s"$first $last" } object Name { def fromString (name: String): Either[String, Name] = { if (name.trim.isEmpty || name.trim.length < 3) Left("Invalid name") else { val first :: last :: Nil = name.split(" ").toList Right(new Name(first, last)) } } }
Ou peut-être ceci:
case class Name private (name: String) { lazy val first :: last :: Nil = name.split(" ").toList } object Name { def fromString (name: String): Either[String, Name] = { if (name.trim.isEmpty || name.trim.length < 3) Left("Invalid name") else Right(new Name(name.trim)) } }
Dans scala, il est plus idiomatique de représenter les cas d'échec en utilisant Soit que par héritage. Si vous avez une instance de N
, vous ne pouvez appeler aucune fonction dessus, vous devrez probablement la faire correspondre. Mais un type comme Sither
est déjà livré avec des fonctions telles que map
, fold
, etc. qui facilite le travail.
Avoir un constructeur privé permet de garantir que vous ne pouvez créer qu'un Name
valide car le seul moyen d'en créer un est d'utiliser la méthode fromString
.
N'utilisez PAS d'implicits pour cela. Il n'y a pas besoin et ne ferait que créer du code déroutant. Pas vraiment à quoi servent les implicits.
Je vois mais le pattern matching est une solution spécifique. le truc ici est que nous appliquons une certaine logique biz sur l'un des membres de la classe
Notez que l'utilisation de
Name (...)
dans la méthodeapply
de l'objet appelleraName.apply (...)
(s'appelle lui-même) et déborder de la pile (je pense que je ne l'ai pas testé) ... vous devriez probablement le remplacer parnew Name (...)
pour vous assurer qu'il appelle directement le constructeur de classe@AlvaroCarrasco bon point, j'ai trop simplifié l'exemple. J'ai édité la question pour éviter la situation comme vous l'avez dit