Interfejs bez skutków ubocznych na szczycie biblioteki stanowej

16

W wywiadzie dla Johna Hughesa, w którym mówi o Erlangu i Haskellu, ma następujące zdanie na temat korzystania z bibliotek stanowych w Erlangu:

Jeśli chcę korzystać z biblioteki stanowej, zwykle buduję na niej interfejs bez efektów ubocznych, aby móc bezpiecznie używać go w pozostałej części kodu.

Co on przez to rozumie? Próbuję wymyślić przykład, jak by to wyglądało, ale zawodzi mnie wyobraźnia i / lub wiedza.

beta
źródło
Cóż, jeśli państwo istnieje, nie zniknie. Sztuką jest stworzenie czegoś, co będzie śledzić zależność. Standardowa odpowiedź Haskella to „monady” lub bardziej zaawansowane „strzałki” . Trochę trudno jest otoczyć ci głowę, a ja tak naprawdę nigdy tego nie zrobiłem, więc ktoś inny musiałby spróbować je wyjaśnić.
Jan Hudec

Odpowiedzi:

12

(Nie znam Erlanga i nie mogę napisać do Haskella, ale myślę, że mimo to mogę odpowiedzieć)

W tym wywiadzie podany jest przykład biblioteki generowania liczb losowych. Oto możliwy stanowy interfejs:

# create a new RNG
var rng = RNG(seed)

# every time we call the next(ceil) method, we get a new random number
print rng.next(10)
print rng.next(10)
print rng.next(10)

Dane wyjściowe mogą być 5 2 7. Dla kogoś, kto lubi niezmienność, jest to po prostu zły! Powinno tak być 5 5 5, ponieważ wywołaliśmy metodę na tym samym obiekcie.

Czym byłby interfejs bezstanowy? Możemy zobaczyć sekwencję liczb losowych jako leniwie ocenianą listę, z której nextfaktycznie pobiera się głowa:

let rng = RNG(seed)
let n : rng = rng in
  print n
  let n : rng = rng in
    print n
    let n : rng in
      print n

Dzięki takiemu interfejsowi zawsze możemy przywrócić poprzedni stan. Jeśli dwa fragmenty twojego kodu odnoszą się do tego samego RNG, faktycznie otrzymają tę samą sekwencję liczb. W funkcjonalnym sposobie myślenia jest to bardzo pożądane.

Wdrożenie tego w stanowym języku nie jest takie skomplikowane. Na przykład:

import scala.util.Random
import scala.collection.immutable.LinearSeq

class StatelessRNG (private val statefulRNG: Random, bound: Int) extends LinearSeq[Int] {
  private lazy val next = (statefulRNG.nextInt(bound), new StatelessRNG(statefulRNG, bound))

  // the rest is just there to satisfy the LinearSeq trait
  override def head = next._1
  override def tail = next._2
  override def isEmpty = false
  override def apply(i: Int): Int = throw new UnsupportedOperationException()
  override def length = throw new UnsupportedOperationException()
}

// print out three nums
val rng = new StatelessRNG(new Random(), 10)
rng.take(3) foreach (n => println(n))

Po dodaniu odrobiny cukru syntaktycznego, aby wyglądał jak lista, jest to całkiem niezłe.

amon
źródło
1
Jeśli chodzi o twój pierwszy przykład: rnd.next (10) generowanie różnych wartości za każdym razem nie ma tyle wspólnego z niezmiennością, ile z definicją funkcji: funkcje muszą być od 1 do 1. (+1, dobre rzeczy)
Steven Evers
Dzięki! To było naprawdę miłe, wnikliwe wyjaśnienie i przykład.
beta
1

Kluczową koncepcją jest tutaj zewnętrzny stan zmienny . Biblioteka, która nie ma zewnętrznego stanu zmiennego, jest wolna od skutków ubocznych. Każda funkcja w takiej bibliotece zależy tylko od przekazanych do niej argumentów.

  • Jeśli twoja funkcja uzyskuje dostęp do dowolnego zasobu, który nie został przez nią utworzony (np. Jako parametr), zależy to od stanu zewnętrznego .
  • Jeśli twoja funkcja tworzy coś, czego nie przekazuje dzwoniącemu (i nie niszczy), twoja funkcja tworzy stan zewnętrzny.
  • Gdy stan zewnętrzny z góry może mieć różne wartości w różnych momentach, wówczas jest zmienny .

Przydatne testy lakmusowe, których używam:

  • jeśli funkcja A musi zostać uruchomiona przed funkcją B, wówczas A tworzy stan zewnętrzny, od którego zależy B.
  • jeśli funkcja, którą piszę, nie może zostać zapamiętana, zależy to od zewnętrznego stanu zmiennego. (Zapamiętywanie może nie być dobrym pomysłem z powodu presji pamięci, ale nadal powinno być możliwe)
Steven Evers
źródło