Mélange d'attributs et d'accesseurs privés et publics dans Raku

12
#Private attribute example
class C { 
    has $!w;                            #private attribute
    multi method w { $!w }              #getter method
    multi method w ( $_ ) {                 #setter method
        warn “Don’t go changing my w!”;   #some side action
        $!w = $_
    }  
}
my $c = C.new
$c.w( 42 )
say $c.w #prints 42
$c.w: 43
say $c.w #prints 43

#but not
$c.w = 44
Cannot modify an immutable Int (43)

jusqu'à présent, si raisonnable, puis

#Public attribute example
class C { 
    has $.v is rw    #public attribute with automatic accessors
}
my $c = C.new
$c.v = 42
say $c.v #prints 42

#but not
$c.v( 43 ) #or $c.v: 43
Too many positionals passed; expected 1 argument but got 2

J'aime l'immédiateté de l'affectation «=», mais j'ai besoin de la facilité de se lier aux actions secondaires fournies par les méthodes multiples. Je comprends que ce sont deux mondes différents, et qu'ils ne se mélangent pas.

MAIS - je ne comprends pas pourquoi je ne peux pas simplement aller $ cv (43) pour définir un attribut public

  1. Je sens que le raku me guide pour ne pas mélanger ces deux modes - certains attributs privés et certains publics et que la pression est vers la méthode (avec certains: le sucre du côlon) - est-ce l'intention du design de Raku?
  2. Suis-je en train de manquer quelque chose?
p6steve
la source
Vous pouvez utiliser un proxy
user0721090601
Comment pourriez-vous utiliser un proxy? Je veux dire, l'accesseur retourne déjà un conteneur quand is rwest spécifié. Le renvoi d'un proxy ne changera pas le nombre de paramètres autorisés sur l'accesseur.
Elizabeth Mattijsen
@ElizabethMattijsen J'ai peut-être mal compris la question. Mais cela semble fonctionner pour ce qu'il veut (activer les deux = fooet .(foo)pour le réglage) et permettre aux effets secondaires de se faire dans les deux cas (mais pas lorsqu'ils sont récupérés uniquement): tio.run/…
user0721090601

Réponses:

13

est-ce l'intention du design de Raku?

Il est juste de dire que Raku n'est pas entièrement dépourvu d'opinion dans ce domaine. Votre question touche à deux thèmes de la conception de Raku, qui méritent tous deux une petite discussion.

Raku a des valeurs l de première classe

Raku utilise abondamment les valeurs L étant une chose de première classe. Quand on écrit:

has $.x is rw;

La méthode générée est:

method x() is rw { $!x }

L' is rwici indique que la méthode renvoie une valeur l , c'est-à-dire quelque chose qui peut être assigné. Ainsi, lorsque nous écrivons:

$obj.x = 42;

Ce n'est pas du sucre syntaxique: c'est vraiment un appel de méthode, puis l'opérateur d'affectation appliqué au résultat. Cela fonctionne, car l'appel de méthode renvoie le Scalarconteneur de l'attribut, qui peut ensuite être affecté à. On peut utiliser la liaison pour diviser cela en deux étapes, pour voir que ce n'est pas une transformation syntaxique triviale. Par exemple, ceci:

my $target := $obj.x;
$target = 42;

Serait l'attribution à l'attribut d'objet. Ce même mécanisme est à l'origine de nombreuses autres fonctionnalités, notamment l'attribution de liste. Par exemple, ceci:

($x, $y) = "foo", "bar";

Fonctionne en construisant un Listcontenant les conteneurs $xet $y, puis l'opérateur d'affectation dans ce cas itère chaque côté par paire pour effectuer l'affectation. Cela signifie que nous pouvons y utiliser des rwaccesseurs d'objets:

($obj.x, $obj.y) = "foo", "bar";

Et tout cela fonctionne naturellement. C'est également le mécanisme derrière l'attribution aux tranches de tableaux et de hachages.

On peut également l'utiliser Proxypour créer un conteneur de valeur l où le comportement de lecture et d'écriture est sous votre contrôle. Ainsi, vous pourriez mettre les actions secondaires enSTORE . Toutefois...

Raku encourage les méthodes sémantiques sur les "colons"

Lorsque nous décrivons OO, des termes comme «encapsulation» et «masquage de données» reviennent souvent. L'idée clé ici est que le modèle d'état à l'intérieur de l'objet - c'est-à-dire la façon dont il choisit de représenter les données dont il a besoin pour implémenter ses comportements (les méthodes) - est libre d'évoluer, par exemple pour gérer de nouvelles exigences. Plus l'objet est complexe, plus cela devient libérateur.

Cependant, les getters et setters sont des méthodes qui ont une connexion implicite avec l'État. Bien que nous puissions prétendre que nous réalisons la dissimulation des données parce que nous appelons une méthode, sans accéder directement à l'état, mon expérience est que nous nous retrouvons rapidement à un endroit où le code extérieur effectue des séquences d'appels de setter pour effectuer une opération - ce qui est une forme de la fonctionnalité envie anti-modèle. Et si nous faisons cela , il est assez certain que nous allons finir avec la logique en dehors de l'objet qui fait un mélange d'opérations getter et setter pour réaliser une opération. Vraiment, ces opérations auraient dû être exposées comme des méthodes avec un nom qui décrit ce qui est réalisé. Cela devient encore plus important si nous sommes dans un environnement simultané; un objet bien conçu est souvent assez facile à protéger à la frontière de la méthode.

Cela dit, de nombreuses utilisations de classsont vraiment des types d'enregistrement / produit: elles existent pour simplement regrouper un ensemble d'éléments de données. Ce n'est pas un hasard si le .sceau ne génère pas seulement un accesseur, mais aussi:

  • Fait en sorte que l'attribut soit défini par la logique d'initialisation d'objet par défaut (c'est-à-dire que a class Point { has $.x; has $.y; }peut être instancié comme Point.new(x => 1, y => 2)), et le rend également dans le.raku méthode de vidage.
  • Place l'attribut dans l' .Captureobjet par défaut , ce qui signifie que nous pouvons l'utiliser dans la déstructuration (par exemple sub translated(Point (:$x, :$y)) { ... }).

Quelles sont les choses que vous souhaiteriez si vous écriviez dans un style plus procédural ou fonctionnel et utilisiez class comme moyen de définir un type d'enregistrement.

La conception Raku n'est pas optimisée pour faire des choses intelligentes dans les setters, car cela est considéré comme une mauvaise chose à optimiser. C'est au-delà de ce qui est nécessaire pour un type d'enregistrement; dans certaines langues, nous pourrions dire que nous voulons valider ce qui est attribué, mais dans Raku, nous pouvons nous tourner vers des subsettypes pour cela. En même temps, si nous faisons vraiment une conception OO, nous voulons une API de comportements significatifs qui cache le modèle d'état, plutôt que de penser en termes de getters / setters, ce qui a tendance à conduire à un échec de colocalisation les données et le comportement, ce qui est de toute façon la raison de faire de l'OO.

Jonathan Worthington
la source
Bon point sur la mise en garde du Proxys (même si je l'ai suggéré ha). La seule fois où je les ai trouvés terriblement utiles, c'est pour moi LanguageTag. En interne, le $tag.regionrenvoie un objet de type Region(car il est stocké en interne), mais la réalité est, il est infiniment plus pratique pour les gens de dire $tag.region = "JP"plus $tag.region.code = "JP". Et ce n'est vraiment que temporaire jusqu'à ce que je puisse exprimer une contrainte Strdans le type, par exemple, has Region(Str) $.region is rw(qui nécessite deux fonctionnalités distinctes planifiées mais de faible priorité)
user0721090601
Merci @Jonathan d'avoir pris le temps d'élaborer la logique de conception - j'avais suspecté une sorte d'aspect huile et eau et pour un corps non CS comme moi, j'ai vraiment la distinction entre une OO correcte avec un état caché et l'application de classes es comme une façon plus conviviale de construire des détenteurs de données ésotériques qui prendraient un doctorat en C ++. Et à user072 ... pour vos réflexions sur Proxy s. Je connaissais les Proxy auparavant, mais je soupçonnais qu'ils (et / ou les traits) sont une syntaxe délibérément assez onéreuse pour décourager le mélange d'huile et d'eau ...
p6steve
p6steve: Raku a été vraiment conçu pour rendre les choses les plus courantes / faciles super faciles. Lorsque vous vous écartez des modèles courants, c'est toujours possible (je pense que vous avez vu environ trois façons différentes de faire ce que vous voulez jusqu'à présent ... et il y en a certainement plus), mais cela vous fait travailler un peu - juste assez pour faire sûr que ce que vous faites est vraiment que vous voulez. Mais via des traits, des procurations, des argots, etc., vous pouvez le faire afin que vous n'ayez besoin que de quelques caractères supplémentaires pour vraiment activer des trucs sympas lorsque vous en avez besoin.
user0721090601
7

MAIS - je ne comprends pas pourquoi je ne peux pas simplement aller $ cv (43) pour définir un attribut public

Eh bien, c'est vraiment à l'architecte. Mais sérieusement, non, ce n'est tout simplement pas la façon standard dont Raku fonctionne.

Maintenant, il serait tout à fait possible de créer un Attributetrait dans l' espace de module, quelque chose comme is settable, qui créerait une méthode accesseur alternative qui serait d' accepter une valeur unique pour définir la valeur. Le problème avec cela est, à mon avis, qu'il y a fondamentalement 2 camps dans le monde sur la valeur de retour d'un tel mutateur: cela retournerait-il la nouvelle valeur, ou l' ancienne valeur?

Veuillez me contacter si vous êtes intéressé à implémenter un tel trait dans l'espace module.

Elizabeth Mattijsen
la source
1
Merci @Elizabeth - c'est un angle intéressant - je ne frappe que cette question ici et là et il n'y a pas assez de retour sur investissement pour construire un trait pour faire l'affaire (ou une compétence de ma part). J'essayais vraiment de me familiariser avec les meilleures pratiques de codage et de m'aligner sur cela - en espérant que la conception de Raku serait alignée sur les meilleures pratiques, ce que je suppose.
p6steve
6

Je soupçonne actuellement que vous venez de devenir confus. 1 Avant de toucher à cela, commençons par ce qui ne vous dérange pas:

J'aime l'immédiateté de la =tâche, mais j'ai besoin de la facilité de se bousculer dans les actions secondaires fournies par les méthodes multiples. ... Je ne comprends pas pourquoi je ne peux pas simplement aller $c.v( 43 )pour définir un attribut public

Vous pouvez faire toutes ces choses. C'est-à-dire que vous utilisez l' =affectation, les méthodes multiples et "allez-y $c.v( 43 )", tout en même temps si vous voulez:

class C {
  has $!v;
  multi method v                is rw {                  $!v }
  multi method v ( :$trace! )   is rw { say 'trace';     $!v }
  multi method v ( $new-value )       { say 'new-value'; $!v = $new-value }
}
my $c = C.new;
$c.v = 41;
say $c.v;            # 41
$c.v(:trace) = 42;   # trace
say $c.v;            # 42
$c.v(43);            # new-value
say $c.v;            # 43

Une source possible de confusion 1

Dans les coulisses, has $.foo is rwgénère un attribut et une méthode unique dans le sens de:

has $!foo;
method foo () is rw { $!foo }

Ce qui précède n'est pas tout à fait correct cependant. Compte tenu du comportement que nous constatons, la foométhode de génération automatique du compilateur est en quelque sorte déclarée de telle sorte que toute nouvelle méthode du même nom l' ombre en silence . 2

Donc, si vous voulez une ou plusieurs méthodes personnalisées portant le même nom qu'un attribut, vous devez répliquer manuellement la méthode générée automatiquement si vous souhaitez conserver le comportement dont elle serait normalement responsable.

Notes de bas de page

1 Voir la réponse de jnthn pour un compte rendu clair, complet et faisant autorité de l'opinion de Raku sur les getters / setters privés vs publics et ce qu'il fait dans les coulisses lorsque vous déclarez des getters / setters publics (c.-à-d. Écrire has $.foo).

2 Si une méthode d'accesseur générée automatiquement pour un attribut était déclarée only, Raku lèverait, je présume, une exception si une méthode portant le même nom était déclarée. Si elle a été déclarée multi, elle ne doit pas être masquée si la nouvelle méthode a également été déclarée multiet doit lever une exception dans le cas contraire. Ainsi, l'accesseur généré automatiquement est déclaré avec ni onlyni, multimais à la place d'une manière qui permet l'observation silencieuse.

raiph
la source
Aha - merci @raiph - c'est la chose qui me manquait. Maintenant, c'est logique. Par Jnthn, j'essaierai probablement d'être un meilleur vrai codeur OO et de garder le style de définition pour les conteneurs de données purs. Mais c'est bon de savoir que c'est dans la boîte à outils!
p6steve