Misturando atributos públicos e privados e acessadores em 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)

até agora, tão razoável, e então

#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

Gosto da iminência da atribuição '=', mas preciso da facilidade de participar de ações paralelas que os vários métodos fornecem. Entendo que esses são dois mundos diferentes e que eles não se misturam.

MAS - Eu não entendo por que não posso simplesmente ir $ cv (43) Para definir um atributo público

  1. Eu sinto que o raku está me guiando para não misturar esses dois modos - alguns atributos privados e outros públicos e que a pressão é direcionada ao método (com alguns: açúcar do cólon) - é essa a intenção do design de Raku?
  2. Estou esquecendo de algo?
p6steve
fonte
Você poderia usar um Proxy
user0721090601
Como você pode usar um proxy? Quero dizer, o acessador já retorna um contêiner quando is rwé especificado. O retorno de um proxy não altera o número de parâmetros permitidos no acessador.
Elizabeth Mattijsen
@ElizabethMattijsen Talvez eu tenha entendido mal a pergunta. Mas isso parece funcionar para o que ele deseja (habilitando ambos = fooe .(foo)para definir) e permitindo que os efeitos colaterais sejam feitos nos dois casos (mas não quando buscados apenas): tio.run/…
user0721090601

Respostas:

13

Essa é a intenção do design de Raku?

É justo dizer que Raku não é totalmente imparcial nessa área. Sua pergunta aborda dois temas no design de Raku, que merecem uma pequena discussão.

Raku tem valores l de primeira classe

Raku faz uso abundante dos valores l, sendo uma coisa de primeira classe. Quando escrevemos:

has $.x is rw;

O método que é gerado é:

method x() is rw { $!x }

O is rwaqui indica que o método está retornando um valor l - ou seja, algo que pode ser atribuído. Assim, quando escrevemos:

$obj.x = 42;

Isso não é açúcar sintático: é realmente uma chamada de método e, em seguida, o operador de atribuição sendo aplicado ao resultado dela. Isso funciona, porque a chamada do método retorna o Scalarcontêiner do atributo, o qual pode ser atribuído. Pode-se usar a ligação para dividir isso em duas etapas, para ver que não é uma transformação sintática trivial. Por exemplo, isto:

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

Estaria atribuindo ao atributo de objeto. Esse mesmo mecanismo está por trás de vários outros recursos, incluindo a atribuição de listas. Por exemplo, isto:

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

Trabalha construindo um Listcontendo os contêineres $xe $y, em seguida, o operador de atribuição, neste caso, itera cada lado em pares para realizar a atribuição. Isso significa que podemos usar rwacessadores de objetos lá:

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

E tudo funciona naturalmente. Esse também é o mecanismo por trás da atribuição a fatias de matrizes e hashes.

Também se pode usar Proxypara criar um contêiner de valor l onde o comportamento de ler e escrever está sob seu controle. Assim, você pode colocar as ações secundárias STORE. Contudo...

Raku incentiva métodos semânticos sobre "setters"

Quando descrevemos OO, termos como "encapsulamento" e "ocultação de dados" costumam aparecer. A idéia principal aqui é que o modelo de estado dentro do objeto - ou seja, a maneira como escolhe representar os dados necessários para implementar seus comportamentos (os métodos) - é livre para evoluir, por exemplo, para lidar com novos requisitos. Quanto mais complexo o objeto, mais libertador ele se torna.

No entanto, getters e setters são métodos que têm uma conexão implícita com o estado. Embora possamos afirmar que estamos conseguindo ocultar dados porque estamos chamando um método, e não acessando diretamente o estado, minha experiência é que terminamos rapidamente em um local onde o código externo está fazendo sequências de chamadas de setter para realizar uma operação - o que é uma forma do recurso inveja o antipadrão. E se fizermos isso , é certo que acabaremos com lógica fora do objeto que faz uma mistura de operações getter e setter para conseguir uma operação. Realmente, essas operações deveriam ter sido expostas como métodos com nomes que descrevem o que está sendo alcançado. Isso se torna ainda mais importante se estivermos em um cenário simultâneo; um objeto bem projetado geralmente é bastante fácil de proteger no limite do método.

Dito isto, muitos usos de classsão realmente tipos de registro / produto: eles existem para simplesmente agrupar vários itens de dados. Não é por acaso que o .sigilo não gera apenas um acessador, mas também:

  • Opta que o atributo seja definido pela lógica de inicialização do objeto padrão (ou seja, a class Point { has $.x; has $.y; }pode ser instanciada como Point.new(x => 1, y => 2)) e também a processa no .rakumétodo de dumping.
  • Opta o atributo no .Captureobjeto padrão , o que significa que podemos usá-lo na desestruturação (por exemplo sub translated(Point (:$x, :$y)) { ... }).

Quais são as coisas que você gostaria se estivesse escrevendo em um estilo mais processual ou funcional e usando classcomo um meio para definir um tipo de registro.

O design do Raku não é otimizado para fazer coisas inteligentes nos levantadores, porque isso é considerado ruim para se otimizar. Está além do necessário para um tipo de registro; em alguns idiomas, poderíamos argumentar que queremos validar o que está sendo atribuído, mas em Raku podemos recorrer a subsettipos para isso. Ao mesmo tempo, se estamos realmente fazendo um projeto de OO, queremos uma API de comportamentos significativos que oculte o modelo de estado, em vez de pensar em termos de getters / setters, que tendem a levar a uma falha na colocação dados e comportamento, que é muito importante para fazer OO de qualquer maneira.

Jonathan Worthington
fonte
Bom ponto de advertência Proxyes (embora eu tenha sugerido isso ha). A única vez que os achei terrivelmente úteis é para mim LanguageTag. Internamente, o $tag.regionretorna um objeto do tipo Region(como ele é armazenado internamente), mas a realidade é que é infinitamente mais conveniente para as pessoas para dizer $tag.region = "JP"mais $tag.region.code = "JP". E isso é apenas temporário até que eu possa expressar uma coerção do Strtipo, por exemplo, has Region(Str) $.region is rw(que requer dois recursos separados, de baixa prioridade, planejados)
user0721090601
Obrigado @ Jonathan por dedicar um tempo para elaborar a lógica do design - eu suspeitava de algum tipo de aspecto de óleo e água e, para um corpo não-CS como eu, realmente entendo a distinção entre OO adequado com estado oculto e a aplicação de classes es como uma maneira mais amigável de criar titulares de dados esotéricos que dariam um doutorado em C ++. E para user072 ... por seus pensamentos sobre Proxy s. Eu sabia sobre os Proxy antes, mas suspeitava que eles (e / ou características) são uma sintaxe deliberadamente bastante onerosa para desencorajar a mistura de óleo e água ...
p6steve 10/01
p6steve: O Raku foi realmente projetado para tornar as coisas mais comuns / fáceis super fáceis. Quando você se desvia dos modelos comuns, é sempre possível (acho que você já viu três maneiras diferentes de fazer o que deseja até agora ... e definitivamente há mais), mas isso faz você trabalhar um pouco - apenas o suficiente para fazer Certifique-se de que você realmente está querendo. Mas, através de características, proxies, gírias, etc., você pode fazer isso, de modo que você só precisa de alguns caracteres extras para realmente habilitar algumas coisas legais quando precisar.
user0721090601 10/01
7

MAS - Eu não entendo por que não posso simplesmente ir $ cv (43) Para definir um atributo público

Bem, isso depende do arquiteto. Mas, falando sério, não, essa simplesmente não é a maneira padrão como Raku trabalha.

Agora, seria perfeitamente possível criar um Attributetraço no espaço módulo, algo como is settable, que iria criar um método de acesso alternativo que faria aceitar um valor único para definir o valor. O problema de fazer isso no núcleo é: acho que existem basicamente dois campos no mundo sobre o valor de retorno de um mutador: retornaria o novo valor ou o valor antigo ?

Entre em contato comigo se você estiver interessado em implementar essa característica no espaço do módulo.

Elizabeth Mattijsen
fonte
11
Obrigado @Elizabeth - esse é um ângulo interessante - só estou respondendo a essa pergunta aqui e ali e não há retorno suficiente na construção de uma característica para executar o truque (ou habilidade da minha parte). Eu estava realmente tentando entender as melhores práticas de codificação e me alinhar a isso - e esperando que o design de Raku estivesse alinhado às melhores práticas, o que eu acho que é.
p6steve 10/01
6

Atualmente, suspeito que você acabou de ficar confuso. 1 Antes de abordar isso, vamos começar com o que você não está confuso:

Eu gosto do imediatismo da =tarefa, mas preciso da facilidade de me envolver em ações paralelas que os vários métodos fornecem. ... Eu não entendo por que não posso simplesmente ir $c.v( 43 )Para definir um atributo público

Você pode fazer todas essas coisas. Ou seja, você usa =atribuição e vários métodos e "apenas vai $c.v( 43 )", tudo ao mesmo tempo, se você quiser:

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

Uma possível fonte de confusão 1

Nos bastidores, has $.foo is rwgera um atributo e um único método ao longo das linhas de:

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

O exposto acima não está certo. Dado o comportamento que estamos vendo, gerada automaticamente do compilador foométodo é de alguma forma ser declarado de tal forma que qualquer novo método de mesmo nome em silêncio sombras de TI. 2

Portanto, se você deseja um ou mais métodos personalizados com o mesmo nome que um atributo, deve replicar manualmente o método gerado automaticamente se desejar manter o comportamento pelo qual ele normalmente seria responsável.

Notas de rodapé

1 Veja a resposta da jnthn para obter uma contabilidade clara, completa e autorizada da opinião de Raku sobre getters / setters privados x públicos e o que ele faz nos bastidores quando você declara getters / setters públicos (por exemplo, gravação has $.foo).

2 Se um método acessador gerado automaticamente para um atributo fosse declarado only, presumo que Raku lançaria uma exceção se um método com o mesmo nome fosse declarado. Se ele foi declarado multi, não deve ser ocultado se o novo método também foi declarado multie, caso contrário, deve gerar uma exceção. Portanto, o acessador gerado automaticamente está sendo declarado com nem onlynem multimas de alguma forma que permita sombreamento silencioso.

raiph
fonte
Aha - obrigado @raiph - foi o que senti que faltava. Agora faz sentido. Por dia, provavelmente tentarei ser um melhor codificador OO verdadeiro e manter o estilo setter para contêineres de dados puros. Mas é bom saber que isso está na caixa de ferramentas!
p6steve 10/01