Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

The Death of Final Tagless

2,728 views

Published on

Final tagless. The topic strikes fear into the hearts of Scala developers everywhere—and not without reason. Final tagless allows developers to build composable Domain Specific Languages (DSLs) that model interaction with the outside world. Programs written using the final tagless style can be tested deterministically and reasoned about at compile-time. Yet the technique requires confusing, compiler-choking higher-kinded types, like `F[_]`, and pervasive, non-inferable context bounds like `F[_]: Concurrent: Console: Logging`. Many have looked at final tagless and wondered if all the layers of complexity and ceremony are really worth the benefits.

In this presentation, John A. De Goes provides a gentle and accessible introduction to final tagless, explaining what it is and the problem it intends to solve. John shows that while final tagless is easier to use than free monads, the technique suffers from a litany of drawbacks that push developers away from functional programming in Scala. John then introduces a novel approach that shares some of the benefits of final tagless, but which is idiomatic Scala, easy to explain, doesn’t need any complex type machinery, provides flawless type inference, and works beautifully across Scala 2.x and Scala 3.

Come join John for an evening of fun as you learn how to write functional code in Scala that's easy to test and easy to reason about—all without the complexity of free monads or final tagless.

Published in: Technology

The Death of Final Tagless

  1. 1. The Death of Tagless Final John A. De Goes — @jdegoes Wiem Zine Elabidine — @wiemzin Amsterdam.scala & FP AMS Xebia, Amsterdam March 2, 2019
  2. 2. SPECIAL THANKS Amsterdam.scala FP AMS
  3. 3. First-Class Effects Tagless Final ZIO Environment Wrap Up
  4. 4. FIRST-CLASS EFFECTS
  5. 5. PROGRAMS AS STATEMENTS def main: Unit = { println("Good morning, what is your name?") val name = readLine() println(s"Good to meet you, $name!") }
  6. 6. PROGRAMS AS STATEMENTS val example: Double = … val value = example val values = List.fill(10)(value) val plusone = value => value + 1
  7. 7. PROGRAMS AS STATEMENTS def main: Unit = … val program = main val programs = List.fill(10)(main) val retried = program => program.retried(2.times)
  8. 8. BENEFITS OF VALUES Abstraction Friendly Refactor Friendly Test Friendly
  9. 9. BENEFITS OF VALUES def main: Unit = { println("Good morning, what is your name?") val name = readLine() println(s"Good to meet you, $name!") } Procedural Effects Functional Effects
  10. 10. Functional effects are immutable data structures that merely describe sequences of operations. At the end of the world, the data structure has to be impurely “interpreted” to real world effects.
  11. 11. println("Hello World") Procedural Effects “Do”
  12. 12. case class PrintLine(text: String) Functional Effects “Describe”
  13. 13. PROGRAMS AS VALUES IO[A] A description of an effect that when unsafely interpreted, will succeed with a value of type A
  14. 14. PROGRAMS AS VALUES class IO[+A](val unsafeInterpret: () => A) { s => def map[B](f: A => B) = flatMap(f.andThen(IO.effect(_))) def flatMap[B](f: A => IO[B]): IO[B] = IO.effect(f(s.unsafeInterpret()).unsafeInterpret()) } object IO { def effect[A](eff: => A) = new IO(() => eff) }
  15. 15. PROGRAMS AS VALUES def putStrLn(line: String): IO[Unit] = IO.effect(println(line)) val getStrLn: IO[String] = IO.effect(scala.io.StdIn.readLine())
  16. 16. PROGRAMS AS VALUES val main = for { _ <- putStrLn("Good morning, " + "what is your name?") name <- getStrLn _ <- putStrLn(s"Good to meet you, $name!") } yield ()
  17. 17. PROGRAMS AS VALUES val main = … val program = main val programs = List.fill(10)(main) val retried = program => program.retried(2.times)
  18. 18. PROGRAMS AS VALUES def loadTest(url: String, n: Int) = val policy = Schedule.recurs(10).jittered val worker = client.get(url).retry(policy) val workers = List.fill(n)(worker) IO.collectAllPar(workers) }
  19. 19. PROGRAMS AS VALUES def loadTest(url: String, n: Int) = val policy = Schedule.recurs(10).jittered val worker = client.get(url).retry(policy) val workers = List.fill(n)(worker) IO.collectAllPar(workers) }
  20. 20. PROGRAMS AS VALUES def loadTest(url: String, n: Int) = val policy = Schedule.recurs(10).jittered val worker = client.get(url).retry(policy) val workers = List.fill(n)(worker) IO.collectAllPar(workers) }
  21. 21. PROGRAMS AS VALUES def loadTest(url: String, n: Int) = val policy = Schedule.recurs(10).jittered val worker = client.get(url).retry(policy) val workers = List.fill(n)(worker) IO.collectAllPar(workers) }
  22. 22. Functional Effects Abstraction Friendly Refactor Friendly Test Friendly
  23. 23. But!
  24. 24. Functional Effects Abstraction Friendly Refactor Friendly Test Friendly
  25. 25. PROGRAMS AS (OPAQUE) VALUES def putStrLn(line: String): IO[Unit] = IO.effect(println(line)) val getStrLn: IO[String] = IO.effect(scala.io.StdIn.readLine())
  26. 26. TAGLESS-FINAL
  27. 27. BACK TO BASICS: JAVA 101 interface Console { void println(String line); String readLine(); }
  28. 28. BACK TO BASICS: JAVA 101 public void program(Console console) { console.println("Good morning, " + "what is your name?"); String name = console.readLine(); console.println(s"Good to meet you, $name!"); }
  29. 29. BACK TO BASICS: JAVA 101 class LiveConsole implements Console { … } class TestConsole implements Console { … } // In production: program(new LiveConsole()); // In tests: program(new TestConsole());
  30. 30. Purely!
  31. 31. IO INTERFACES trait Console { def println(line: String): IO[Unit] val readLine: IO[String] }
  32. 32. PURE PROGRAM WITH INTERFACE def program(c: Console) = for { _ <- c.println("Good morning, " + "what is your name?") name <- c.readLine _ <- c.println(s"Good to meet you, $name!") } yield ()
  33. 33. POLYMORPHIC EFFECT trait Console[IO[_]] { def println(String line): IO[Unit] val readLine: IO[String] }
  34. 34. MAKING IT EVEN MORE OBSCURE trait Console[F[_]] { def println(String line): F[Unit] val readLine: F[String] }
  35. 35. THE GLORIOUS TAGLESS-FINAL TYPE CLASS trait Console[F[_]] { def println(String line): F[Unit] val readLine: F[String] } object Console { def apply[F[_]](implicit F: Console[F]) = F }
  36. 36. EFFECT-POLYMORPHIC PROGRAMS def program[F[_]: Console: Monad] = for { _ <- Console[F].println( "Good morning, " + "what is your name?") name <- Console[F].readLine _ <- Console[F].println( s"Good to meet you, $name!") } yield ()
  37. 37. TEST VS PRODUCTION EFFECTS case class TestIO[A](...) implicit val TestConsole = new Console[TestIO] { … } implicit val LiveConsole = new Console[IO] { … } // In production: program[IO] : IO[Unit] // In tests: program[TestIO] : TestIO[Unit]
  38. 38. Functional Effects Abstraction Friendly Refactor Friendly Test Friendly
  39. 39. Functional Effects Abstraction Friendly Refactor Friendly Test Friendly
  40. 40. TAGLESS-FINAL IS EASY! Functional Effects IO[A]
  41. 41. TAGLESS-FINAL IS PRETTY EASY! Functional Effects Parametric Polymorphism F
  42. 42. TAGLESS-FINAL IS EASY-ISH! Functional Effects Parametric Polymorphism F[_] Higher-Kinded Types
  43. 43. TAGLESS-FINAL ISN’T THAT HARD! Functional Effects Parametric Polymorphism trait Console[F[_]] { … } Higher-Kinded Types Type Classes
  44. 44. TAGLESS-FINAL COULD BE MUCH HARDER! Functional Effects Parametric Polymorphism implicit val TestConsole = new Console[TestIO] { … } Higher-Kinded Types Type Classes Type Class Instances
  45. 45. TAGLESS-FINAL IS SORT OF HARD! Functional Effects Parametric Polymorphism new Console[({type F[A] = State[TestData, A]})#F] { … } Higher-Kinded Types Type Classes Type Class Instances Partial Type Application
  46. 46. OMG, TAGLESS-FINAL IS THE WORST!!! Functional Effects Parametric Polymorphism def program[F[_]: Monad: Console: Database] = ... Higher-Kinded Types Type Classes Type Class Instances Partial Type Application Monad Hierarchy
  47. 47. TAGLESS-FINAL IS MANY THINGS EASY IS NOT ONE OF THEM
  48. 48. ☠ TYPE CLASS ABUSE ☠ THE DARK SIDE OF TAGLESS-FINAL REAL TYPE CLASS Defines common structure across types through lawful operations, enabling abstraction. FAKE TYPE CLASS Defines a common interface across types through ad hoc polymorphism, enabling testing.
  49. 49. ☠ TYPE CLASS ABUSE ☠ THE DARK SIDE OF TAGLESS-FINAL REAL TYPE CLASS Defines common structure across types through lawful operations, enabling abstraction. FAKE TYPE CLASS Defines a common interface across types through ad hoc polymorphism, enabling testing.
  50. 50. ☠ BIG BANG INTRODUCTION ☠ THE DARK SIDE OF TAGLESS-FINAL def program[F[_]: Console: Monad] = for { _ <- Console[F].println( "Good morning, " + "what is your name?") name <- Console[F].readLine _ <- Console[F].println( s"Good to meet you, $name!") } yield ()
  51. 51. ☠ TEDIOUS REPETITION ☠ THE DARK SIDE OF TAGLESS-FINAL def genFeed[F[_]: Monad: Logging: UserDatabase: ProfileDatabase: RedisCache: GeoIPService: AuthService: SessionManager: Localization: Config: EventQueue: Concurrent: Async: MetricsManager]: F[Feed] = ???
  52. 52. ☠ STUBBORN REPETITION ☠ THE DARK SIDE OF TAGLESS-FINAL def genFeed[F[_]: Everything]: F[Feed] = ???
  53. 53. ☠ COMPLETELY NON-INFERABLE ☠ THE DARK SIDE OF TAGLESS-FINAL def genFeed = ???
  54. 54. ☠ FAKE PARAMETRIC GUARANTEES ☠ def innocent[F[_]: Monad]: F[Unit] = { def effect[A](a: => A): F[A] = Monad[F].point(()).map(_ => a) println("What guarantees?") effect(System.exit(42)) } THE DARK SIDE OF TAGLESS-FINAL
  55. 55. THE DEATH OF TAGLESS-FINAL
  56. 56. THE DEATH OF TAGLESS-FINAL
  57. 57. ZIO ENVIRONMENT
  58. 58. ZIO is a zero-dependency Scala library for asynchronous and concurrent programming.
  59. 59. High-Performance Γ Type-Safe ǁ Concurrent ⇅ Asynchronous Resource-Safe ✓ Testable Resilient λ Functional
  60. 60. BACK TO BASICS trait Console { def println(line: String): IO[Unit] val readLine: IO[String] }
  61. 61. BACK TO BASICS def program(c: Console) = for { _ <- c.println("Good morning, " + "What is your name?") name <- c.readLine _ <- c.println(s"Good to meet you, $name!") } yield ()
  62. 62. BACK TO BASICS def program(c: Console, p: Persistence) = for { _ <- c.println("Good morning, " + "what is your name?") name <- c.readLine _ <- p.savePreferences(name) _ <- c.println(s"Good to meet you, $name!") } yield ()
  63. 63. BACK TO BASICS def program(s1: Service1, s2: Service2, s3: Service3, … sn: ServiceN) = for { a <- foo(s1, s9, s3)("localhost", 42) b <- bar(sn, s19, s3)(a, 1024) ... } yield z
  64. 64. BACK TO BASICS def program(s1: Service1, s2: Service2, s3: Service3, … sn: ServiceN) = for { a <- foo(s1, s9, s3)("localhost", 42) b <- bar(sn, s19, s3)(a, 1024) ... } yield z
  65. 65. THE MODULE PATTERN trait HasConsole { def console: HasConsole.Service } object HasConsole { trait Service { def println(line: String): IO[Unit] val readLine: IO[String] } }
  66. 66. THE MODULE PATTERN trait HasConsole { def console: HasConsole.Service } object HasConsole { trait Service { def println(line: String): IO[Unit] val readLine: IO[String] } }
  67. 67. THE MODULE PATTERN def program(s: HasConsole with HasPersistence) = for { _ <- s.console.println("What is your name?") name <- s.console.readLine _ <- s.persistence.savePreferences(name) _ <- s.console.println(s"Good to meet” + ” you, $name!") } yield ()
  68. 68. THE MODULE PATTERN def program(s: HasService1 with … HasServiceN) = for { a <- foo(s)("localhost", 42) b <- bar(s)(a, 1024) ... } yield z
  69. 69. THE READER MONAD case class Reader[-R, +A](provide: R => A) { self => def map[B](f: A => B) = flatMap(a => Reader.point(f(a))) def flatMap[R1 <: R, B](f: A => Reader[R1, B]) = Reader[R, B](r => f(self.provide(r)).provide(r)) } object Reader { def point[A](a: => A): Reader[Any, A] = Reader(_ => a) def environment[R]: Reader[R, R] = Reader(identity) }
  70. 70. THE READER MONAD def program: Reader[HasService1 with ..., Unit] = for { a <- foo("localhost", 42) b <- bar(a, 1024) ... } yield z
  71. 71. THE READER MONAD TRANSFORMER ReaderT[IO, R, A]
  72. 72. THE READER MONAD TRANSFORMER ReaderT[IO, R, A] IO ReaderT
  73. 73. ZIO ENVIRONMENT
  74. 74. ZIO ENVIRONMENT
  75. 75. ZIO ENVIRONMENT ZIO[R, E, A]*
  76. 76. ZIO ENVIRONMENT ZIO[R, E, A]* type UIO [ +A] = ZIO[Any, Nothing, A] type Task[ +A] = ZIO[Any, Throwable, A] type IO [+E, +A] = ZIO[Any, E, A] *
  77. 77. ZIO ENVIRONMENT ZIO[R, E, A] Environment Type
  78. 78. ZIO ENVIRONMENT ZIO[R, E, A] Failure Type
  79. 79. ZIO ENVIRONMENT ZIO[R, E, A] Success Type
  80. 80. ZIO ENVIRONMENT ZIO[Console, IOException, Unit] Requires Console
  81. 81. ZIO ENVIRONMENT Might Fail with IOException ZIO[Console, IOException, Unit]
  82. 82. ZIO ENVIRONMENT Might Succeed with Unit ZIO[Console, IOException, Unit]
  83. 83. ZIO ENVIRONMENT: EXAMPLE val program: ZIO[Console with Persistence, IOException, Unit] = for { _ <- putStrLn("Good morning, what is your name?") name <- getStrLn _ <- savePreferences(name) _ <- putStrLn(s"Good to meet you, $name!") } yield ()
  84. 84. ZIO ENVIRONMENT: CORE sealed trait ZIO[-R, +E, +A] { ... def provide(environment: R): ZIO[Any, E, A] = ... } object ZIO { def accessM[R, E, A](f: R => ZIO[R, E, A]): ZIO[R, E, A] = ... def access[R, E, A](f: R => A): ZIO[R, Nothing, A] = accessM(ZIO.succeed(_)) def environment[R]: ZIO[R, Nothing, R] = access(identity) }
  85. 85. ZIO ENVIRONMENT: CORE sealed trait ZIO[-R, +E, +A] { ... def provide(environment: R): ZIO[Any, E, A] = ... } object ZIO { def accessM[R, E, A](f: R => ZIO[R, E, A]): ZIO[R, E, A] = ... def access[R, E, A](f: R => A): ZIO[R, Nothing, A] = accessM(ZIO.succeed(_)) def environment[R]: ZIO[R, Nothing, R] = access(identity) }
  86. 86. ZIO ENVIRONMENT: CORE sealed trait ZIO[-R, +E, +A] { ... def provide(environment: R): ZIO[Any, E, A] = ... } object ZIO { def accessM[R, E, A](f: R => ZIO[R, E, A]): ZIO[R, E, A] = ... def access[R, E, A](f: R => A): ZIO[R, Nothing, A] = accessM(ZIO.succeed(_)) def environment[R]: ZIO[R, Nothing, R] = access(identity) }
  87. 87. ZIO ENVIRONMENT: CORE sealed trait ZIO[-R, +E, +A] { ... def provide(environment: R): ZIO[Any, E, A] = ... } object ZIO { def accessM[R, E, A](f: R => ZIO[R, E, A]): ZIO[R, E, A] = ... def access[R, E, A](f: R => A): ZIO[R, Nothing, A] = accessM(ZIO.succeed(_)) def environment[R]: ZIO[R, Nothing, R] = access(identity) }
  88. 88. ZIO ENVIRONMENT: TUTORIAL // Module (contains service) trait Console { def console: Console.Service }
  89. 89. ZIO ENVIRONMENT: TUTORIAL object Console { // Service definition: trait Service { def println(line: String): ZIO[Any, Nothing, Unit] val readLine: ZIO[Any, IOException, String] } }
  90. 90. ZIO ENVIRONMENT: TUTORIAL object ConsoleLive extends Console { val console = new Console.Service { def println(line: String): UIO[Unit] = ZIO.effectTotal(scala.io.StdIn.println(line)) val readLine: IO[IOException, String] = ZIO.effect(scala.io.StdIn.readLine()).refineOrDie { case e : IOException => e } } }
  91. 91. ZIO ENVIRONMENT: TUTORIAL // Optional (but handy!) helpers: package object console { def println(line: String): ZIO[Console, Nothing, Unit] = ZIO.accessM(_.console println line) val readLine: ZIO[Console, IOException, String] = ZIO.accessM(_.console.readLine) }
  92. 92. ZIO ENVIRONMENT: TUTORIAL val program: ZIO[Console, IOException, Unit] = for { _ <- putStrLn("Good morning, what is your name?") name <- getStrLn _ <- putStrLn(s"Good to meet you, $name!") } yield () DefaultRuntime.unsafeRun(program.provide(ConsoleLive))
  93. 93. ZIO ENVIRONMENT: TUTORIAL object ConsoleTest extends Console { ... } DefaultRuntime.unsafeRun(program.provide(ConsoleTest))
  94. 94. ZIO ENVIRONMENT: TUTORIAL val MyEnvironment: R = ... val MyRuntime: Runtime[R] = Runtime(MyEnvironment, PlatformLive) Your Own R Platform (thread pool, etc.)
  95. 95. ZIO ENVIRONMENT: TEACHABLE trait Console { def console: Console.Service } object Console { trait Service { def println(line: String): ZIO[Any, Nothing, Unit] val readLine: ZIO[Any, IOException, String] } } object ConsoleLive extends Console.Service { def println(line: String) = ZIO.effectTotal(scala.io.StdIn.println(line)) val readLine = ZIO.effect(scala.io.StdIn.readLine()).refineOrDie(JustIOException) } Module
  96. 96. ZIO ENVIRONMENT: TEACHABLE trait Console { def console: Console.Service } object Console { trait Service { def println(line: String): ZIO[Any, Nothing, Unit] val readLine: ZIO[Any, IOException, String] } } object ConsoleLive extends Console.Service { def println(line: String) = ZIO.effectTotal(scala.io.StdIn.println(line)) val readLine = ZIO.effect(scala.io.StdIn.readLine()).refineOrDie(JustIOException) } Service
  97. 97. ZIO ENVIRONMENT: TEACHABLE trait Console { def console: Console.Service } object Console { trait Service { def println(line: String): ZIO[Any, Nothing, Unit] val readLine: ZIO[Any, IOException, String] } } object ConsoleLive extends Console.Service { def println(line: String) = ZIO.effectTotal(scala.io.StdIn.println(line)) val readLine = ZIO.effect(scala.io.StdIn.readLine()).refineOrDie(JustIOException) } Implementation
  98. 98. ZIO ENVIRONMENT: COMPOSABLE trait Console { def console: Console.Service } trait Logging { def logging: Logging.Service } trait Persistence { def persistence: Persistence.Service } ... val program: ZIO[Console with Logging with Persistence, AppError, Unit] = ...
  99. 99. ZIO ENVIRONMENT: PERFORMANT scala.concurrent.Future
  100. 100. ZIO ENVIRONMENT: FULLY INFERABLE val program = for { _ <- putStrLn("Good morning, what is your name?") name <- getStrLn _ <- savePreferences(name) _ <- log.debug("Saved $name to configuration") _ <- putStrLn(s"Good to meet you, $name!") } yield ()
  101. 101. ZIO ENVIRONMENT: FULLY INFERABLE val program: ZIO[Console, Error, Unit] = ... ^^^^^^^ Found: Console Expected: Console with Persistence with Logging with Config with Auth
  102. 102. ZIO ENVIRONMENT: CONCISE trait Console { def console: Console.Service } trait Logging { def logging: Logging.Service } trait Persistence { def persistence: Persistence.Service } ... type ProgramEnv = Console with Logging with Persistence val program: ZIO[ProgramEnv, AppError, Unit] = ...
  103. 103. ZIO ENVIRONMENT: CONCISE trait Console { def console: Console.Service } trait Logging { def logging: Logging.Service } trait Persistence { def persistence: Persistence.Service } ... type Program[A] = ZIO[Console with Logging with Persistence, AppError, A] val program: Program[Unit] = ...
  104. 104. ZIO ENVIRONMENT: MODULAR def fn1: ZIO[R1, E, A] = { def fn2: ZIO[R2, E, B] = ... val localEnvironment: R2 = ... val v1 = fn2.provide(localEnvironment) ... } val globalEnvironment: R1 = ... val v2 = fn1.provide(globalEnvironment) ...
  105. 105. ZIO ENVIRONMENT: INCREMENTAL // Deeply nested code: val myCode: Task[Unit] = … for { ... result <- database.query(q) ... } yield ()
  106. 106. ZIO ENVIRONMENT: INCREMENTAL type TaskDB[A] = ZIO[Database, Throwable, A] // Now fully testable! def myCodeV2: TaskDB[Unit] = … for { ... result <- database.query(q) ... } yield ()
  107. 107. THE REANIMATION OF TAGLESS-FINAL
  108. 108. THE REANIMATION OF TAGLESS-FINAL trait HasState[S] { def state: HasState.Service[S] } object HasState { trait Service[S] { def state: Ref[S] } }
  109. 109. THE REANIMATION OF TAGLESS-FINAL implicit def MSZIO[S, R <: HasState[S], E]: MonadState[ZIO[R, E, ?], S] = new MonadState[ZIO[R, E, ?], S] { ... } ... def program[F[_]: MonadState[S, ?]]: F[Unit] = … program[ZIO[HasState[S], E]]
  110. 110. THE REANIMATION OF TAGLESS-FINAL implicit def MSZIO[S, R <: HasState[S], E]: MonadState[ZIO[R, E, ?], S] = new MonadState[ZIO[R, E, ?], S] { ... } ... def program[F[_]: MonadState[MySt, ?]]: F[Unit] = … program[ZIO[HasState[MySt], Err]]
  111. 111. THE (RE)RISING OF CAKE
  112. 112. THE (RE)RISING OF CAKE // Traditional MONOLITHIC cake trait MyModule extends CacheModule with LoggingModule with DatabaseModule with AuthModule { def refreshCache: Unit = ... def genFeed : Feed = ... def trace : Unit = ... }
  113. 113. THE (RE)RISING OF CAKE // Next-generation MODULAR cake object MyFunctions { val refreshCache: ZIO[Cache, Error, Unit] = ... val genFeed: ZIO[Database with Auth, Error, Feed] = ... val trace: ZIO[Debug, Nothing, Unit] = ... }
  114. 114. WRAP UP
  115. 115. COMPARISON MATRIX Transformers Free Monads Eff Tagless Final Environmental Effects Composable ✔ ✔ ✔ ✔ ✔ Performance 𐄂 𐄂 𐄂 ✔ ✔ Reasoning Up to Discipline Up to Discipline Up to Discipline Up to Discipline Up to Discipline Easily Teachable 𐄂 ? ✔ 𐄂 ✔ Concision 𐄂 𐄂 𐄂 𐄂 ✔ Full Type Inference 𐄂 𐄂 𐄂 𐄂 ✔ Modularity ✔ ✔ ✔ 𐄂 ✔ Pinpoint Testability 𐄂 𐄂 𐄂 𐄂 ✔
  116. 116. GETTING STARTED WITH ZIO ✨ github.com/scalaz/scalaz-zio ✨ gitter.im/scalaz/scalaz-zio ✨ scalaz.github.io/scalaz-zio
  117. 117. Thank You! Any questions? Stalk me online at @jdegoes Follow Wiem online @wiemzin

×