Qual é a motivação para a avaliação da atribuição do Scala à Unidade em vez do valor atribuído?

84

Qual é a motivação para a avaliação da atribuição do Scala à Unidade em vez do valor atribuído?

Um padrão comum na programação de E / S é fazer coisas assim:

while ((bytesRead = in.read(buffer)) != -1) { ...

Mas isso não é possível no Scala porque ...

bytesRead = in.read(buffer)

.. retorna Unit, não o novo valor de bytesRead.

Parece algo interessante deixar de fora de uma linguagem funcional. Estou me perguntando por que isso foi feito?

Graham Lea
fonte
David Pollack postou algumas informações em primeira mão, praticamente endossadas pelo comentário que o próprio Martin Odersky deixou em sua resposta. Acho que se pode aceitar com segurança a resposta de Pollack.
Daniel C. Sobral

Respostas:

88

Defendi que as atribuições devolveriam o valor atribuído em vez da unidade. Martin e eu conversamos sobre isso, mas seu argumento era que colocar um valor na pilha apenas para retirá-lo 95% do tempo era uma perda de códigos de byte e tinha um impacto negativo no desempenho.

David Pollak
fonte
7
Existe uma razão pela qual o compilador Scala não pôde verificar se o valor da atribuição é realmente usado e gerar bytecode eficiente de acordo?
Matt R
43
Não é tão fácil na presença de levantadores: todo levantador tem que retornar um resultado, o que é difícil de escrever. Em seguida, o compilador precisa otimizá-lo, o que é difícil de fazer em todas as chamadas.
Martin Odersky
1
Seu argumento faz sentido, mas java e C # são contra isso. Eu acho que você está fazendo algo estranho com o código de byte gerado, então como seria uma atribuição em Scala sendo compilada em um arquivo de classe e descompilada de volta para Java?
Phương Nguyễn
3
@ PhươngNguyễn A diferença é o Princípio de Acesso Uniforme. Em C # / Java setters (geralmente) retornam void. Em Scala foo_=(v: Foo)deve retornar Foose a atribuição retornar .
Alexey Romanov
5
@Martin Odersky: que tal seguir: setters permanecem void( Unit), atribuições x = valuesão traduzidas em equivalentes de x.set(value);x.get(value); o compilador elimina nas fases de otimização os get-calls se o valor não foi usado. Pode ser uma mudança bem-vinda em uma nova versão principal do Scala (por causa da incompatibilidade com versões anteriores) e menos irritações para os usuários. O que você acha?
Eugen Labun
20

Não tenho acesso a informações privilegiadas sobre os reais motivos, mas minha suspeita é muito simples. O Scala torna os loops de efeito colateral difíceis de usar, de modo que os programadores irão naturalmente preferir as compreensões para.

Ele faz isso de várias maneiras. Por exemplo, você não tem um forloop onde declara e altera uma variável. Você não pode (facilmente) transformar o estado em um whileloop ao mesmo tempo em que testa a condição, o que significa que geralmente é necessário repetir a mutação imediatamente antes e no final dela. As variáveis ​​declaradas dentro de um whilebloco não são visíveis na whilecondição de teste, o que torna do { ... } while (...)muito menos útil. E assim por diante.

Gambiarra:

while ({bytesRead = in.read(buffer); bytesRead != -1}) { ... 

Por qualquer coisa que valha a pena.

Como uma explicação alternativa, talvez Martin Odersky teve que enfrentar alguns insetos muito feios derivados de tal uso e decidiu bani-lo de sua linguagem.

EDITAR

David Pollack tem respondido com alguns fatos reais, que são claramente endossado pelo fato de que Martin Odersky se comentou sua resposta, dando credibilidade ao argumento problemas relacionados ao desempenho apresentadas por Pollack.

Daniel C. Sobral
fonte
3
Então, provavelmente a forversão em loop seria: o for (bytesRead <- in.read(buffer) if (bytesRead) != -1que é ótimo, exceto que não funcionará porque não há foreache está withFilterdisponível!
oxbow_lakes
12

Isso aconteceu como parte do Scala ter um sistema de tipos mais "formalmente correto". Falando formalmente, a atribuição é uma declaração puramente de efeito colateral e, portanto, deve retornar Unit. Isso tem algumas consequências interessantes; por exemplo:

class MyBean {
  private var internalState: String = _

  def state = internalState

  def state_=(state: String) = internalState = state
}

O state_=método retorna Unit(como seria esperado para um setter) precisamente porque a atribuição retorna Unit.

Concordo que, para padrões de estilo C, como copiar um fluxo ou similar, essa decisão de design específica pode ser um pouco problemática. No entanto, é relativamente não problemático em geral e realmente contribui para a consistência geral do sistema de tipos.

Daniel Spiewak
fonte
Obrigado, Daniel. Acho que preferiria que a consistência fosse que as atribuições E os setters retornassem o valor! (Não há razão para que não o façam.) Suspeito que ainda não estou grocando as nuances de conceitos como uma "declaração puramente de efeito colateral".
Graham Lea
2
@ Graham: Mas então, você teria que seguir a consistência e garantir em todos os seus configuradores, por mais complexos que sejam, que eles retornem o valor que definiram. Isso seria complicado em alguns casos e, em outros casos, simplesmente errado, eu acho. (O que você retornaria em caso de erro? Nulo? - ao invés disso não. Nenhum? - então seu tipo será Opção [T].) Acho que é difícil ser consistente com isso.
Debilski
7

Talvez isso se deva ao princípio de separação comando-consulta ?

O CQS tende a ser popular na interseção de estilos de programação OO e funcionais, pois cria uma distinção óbvia entre métodos de objeto que têm ou não efeitos colaterais (ou seja, que alteram o objeto). Aplicar o CQS a atribuições de variáveis ​​está levando mais longe do que o normal, mas a mesma ideia se aplica.

Uma pequena ilustração de por CQS é útil: Considere uma linguagem F / OO híbrido hipotética com uma Listclasse que tem métodos Sort, Append, First, e Length. No estilo OO imperativo, pode-se querer escrever uma função como esta:

func foo(x):
    var list = new List(4, -2, 3, 1)
    list.Append(x)
    list.Sort()
    # list now holds a sorted, five-element list
    var smallest = list.First()
    return smallest + list.Length()

Considerando que em um estilo mais funcional, seria mais provável escrever algo assim:

func bar(x):
    var list = new List(4, -2, 3, 1)
    var smallest = list.Append(x).Sort().First()
    # list still holds an unsorted, four-element list
    return smallest + list.Length()

Eles parecem estar tentando fazer a mesma coisa, mas obviamente um dos dois está incorreto e, sem saber mais sobre o comportamento dos métodos, não podemos dizer qual deles.

Usando o CQS, entretanto, insistiríamos que se Appende Sortalterar a lista, eles devem retornar o tipo de unidade, evitando assim a criação de bugs usando a segunda forma quando não deveríamos. A presença de efeitos colaterais, portanto, também se torna implícita na assinatura do método.

CA McCann
fonte
4

Eu acho que isso é para manter o programa / a linguagem livre de efeitos colaterais.

O que você descreve é ​​o uso intencional de um efeito colateral que, em geral, é considerado uma coisa ruim.

Jens Schauder
fonte
Heh. Scala sem efeitos colaterais? :) Além disso, imagine um caso como val a = b = 1(imagine "mágico" valna frente de b) vs val a = 1; val b = 1;..
Isso não tem nada a ver com efeitos colaterais, pelo menos não no sentido descrito aqui: Efeito colateral (ciência da computação)
Feuermurmel
4

Não é o melhor estilo usar uma atribuição como uma expressão booleana. Você executa duas coisas ao mesmo tempo, o que freqüentemente leva a erros. E o uso acidental de "=" em vez de "==" é evitado com a restrição Scalas.

deamon
fonte
2
Eu acho que esse é um motivo ruim! Conforme postado pelo OP, o código ainda compila e executa: ele simplesmente não faz o que você poderia razoavelmente esperar. É mais um pegadinho, não menos!
oxbow_lakes
1
Se você escrever algo como if (a = b), não será compilado. Portanto, pelo menos esse erro pode ser evitado.
deamon
1
O OP não usou '=' em vez de '==', ele usou ambos. Ele espera que a atribuição retorne um valor que pode então ser usado, por exemplo, para comparar com outro valor (-1 no exemplo)
IttayD
@deamon: ele irá compilar (pelo menos em Java) se a e b forem booleanos. Já vi novatos caindo nessa armadilha usando if (a = true). Mais uma razão para preferir o mais simples if (a) (e mais claro se usar um nome mais significativo!).
PhiLho
2

A propósito: acho o while-trick inicial estúpido, mesmo em Java. Por que não algo assim?

for(int bytesRead = in.read(buffer); bytesRead != -1; bytesRead = in.read(buffer)) {
   //do something 
}

Concedido, a atribuição aparece duas vezes, mas pelo menos bytesRead está no escopo ao qual pertence, e não estou brincando com truques de atribuição engraçados ...

Landei
fonte
1
Embora o truque seja bastante comum, ele geralmente aparece em todos os aplicativos que lêem através de um buffer. E sempre se parece com a versão do OP.
TWiStErRob
0

Você pode ter uma solução alternativa para isso, desde que tenha um tipo de referência para indireção. Em uma implementação ingênua, você pode usar o seguinte para tipos arbitrários.

case class Ref[T](var value: T) {
  def := (newval: => T)(pred: T => Boolean): Boolean = {
    this.value = newval
    pred(this.value)
  }
}

Então, sob a restrição que você terá que usar ref.valuepara acessar a referência posteriormente, você pode escrever seu whilepredicado como

val bytesRead = Ref(0) // maybe there is a way to get rid of this line

while ((bytesRead := in.read(buffer)) (_ != -1)) { // ...
  println(bytesRead.value)
}

e você pode fazer a verificação de bytesReaduma maneira mais implícita, sem precisar digitá-la.

Debilski
fonte