k47.cz
mastodon twitter RSS
bandcamp explorer

Scala - líné parametry, líné závislosti a líné proxy

Scala obsahuje šikovný modifikátor lazy který z obyčejné val proměnné nebo členské proměnné udělá její línou variantu. Ta se vyhodnotí na prvním místě, kde je zmíněna a pak takto vypočtenou hodnotu jenom recykluje.


class Person {
  // final field, stable, computed on object creation
  val dateOfBirth: Date = ???

  // lazy field, stable, computed on first call
  lazy val allPredecessorsUpToApes: Seq[Person] = ???

  // method, not stable, recomputed on every call
  def age: Int = ???

  // truly variable variable
  var name: String = ???

  def test = {
    // lazy local field
    lazy val localLazyVariable = ???
  }
}

Někdy ale může nastat případ, kdy by člověk potřeboval líné parametry funkcí, metod a konstruktorů, ale zápis def func(lazy x: SomeType) není ve Scale validní.

Naštěstí není nic ztraceno, protože líné parametry můžeme vytvořit kombinací líných fieldů a by-name argumentů. Takhle:

def shootHimToTheMoon(byNamePerson: => Person) {
  lazy val lazyPerson = byNamePerson

  lazyPerson // tady se vyhodnotí
  lazyPerson // tady se použije předchozí hodnota

  // ...
}

shootHimToTheMoon(expensiveCreationOfPersonObject())

Funkce expensiveCreationOfPersonObject je zavolána na místě by-name argumentu. To zajistí, že místo vyhodnocení bude „zabalena“ do funkce, která bude vyhodnocena až v těle shootHimToTheMoon a to na každém místě, kde je zmíněna, tedy klidně několikrát nebo také ani jednou.

def test(arg: => Int) = { arg; arg; arg }
test { println("side effect!"); 1 } // vypíše 3x "side effect"

Ale protože je ve funkci shootHimToTheMoon by-name argument hned přiřazen do lazy val a ta je vyhodnocena až při prvním použití, je argument také vyhodnocen jenom jednou nebo vůbec. A to je efektivně líný argument.


Toho se dá snadno využít pro předávání líných závislosti do tříd (jak se to řešilo tady nebo tady):

class HomeController(_loginService: => ILoginService) extends Controller {
  lazy val loginService = _loginService

  def showLogin() = {
    // nepotřebuje loginService, takže se líná hodnota vůbec nebude materializovat
    PartialView()
  }

  def processLogin(credentials: Credentials) = {
    // tady už potřebuje loginService, tak ho použije
    // použití a sémantika lazy val je stejná jako v případě val
    loginService.login(credentials)

    // ... and now for something completely different
  }
}

Velice podobně můžeme konstruovat líné proxy (jako v odkazovaném Augiho článku, který tohle moje snažení nastartoval):

class LoginServiceLazyProxy(accessor: => ILoginService) extends ILoginService {
  lazy val live = accessor // thread safety included

  def login(credentials: Credentials): LoginResult =
    live.login(credentials)
}

Línou proxy pak vytvoříme jednoduše:

val lazyLoginProxy = new LoginServiceLazyProxy(new LoginService)

Pořád platí, že instance LoginService se vytvoří jenom když bude skutečně potřeba.

píše k47, ascii@k47.cz