From 08f3ec54789d745db5188ab738b6fb868afa93fa Mon Sep 17 00:00:00 2001 From: Awethon Date: Fri, 18 Oct 2024 03:24:02 +0000 Subject: [PATCH] deploy: 8c30c03c4832ed0f567e2af0bddacde87ab83508 --- index.json | 2 +- posts/scala-wrappers/index.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/index.json b/index.json index 7e9b5bc..3d97c40 100644 --- a/index.json +++ b/index.json @@ -1 +1 @@ -[{"content":"Scala is a great language with many features and a complex type system. It provides many ways to solve the same problem, but each way has its own pros and cons. Unfortunately, it\u0026rsquo;s a common problem of scala developers to utilize the features of Scala, disregarding the cognitive load that it might bring to the team. In this article, I\u0026rsquo;ll try to advocate for a simple and easy-to-understand way of implementing wrappers in Scala.\nBut what are these wrappers and what are they good for? The wrapper pattern (or the proxy pattern) is a design pattern that allows to add new functionality to an existing class without altering it. It is useful in scenarios where an application needs to be instrumented, meaning it needs to be monitored, metered, and traced. Putting all the instrumentation in business logic will make code less readable by mixing business logic with monitoring code. And that\u0026rsquo;s where wrappers shine. However, they can be used for other purposes as well (like timeouts and retries). (NB: transforming an argument or a return value in a wrapper is considered to be an anti-pattern.)\nThere are two main ways to implement wrappers in Scala: using trait mixins and using classic wrapper classes. In previous edition this article covered a third way of implementing wrappers in Scala using Mid type class from tofu library. But it had many disadvantages and tofu is not maintained anymore. So, I decided to remove it from the article. For some reason trait mixins were widely used in Scala community and in the past using mixins was considered to be \u0026ldquo;The Scala Way\u0026rdquo;. And I still see developers using them. However, I don\u0026rsquo;t think that using mixins is a good idea in most cases. I\u0026rsquo;ll try to explain why in this article.\nTrait mixin One of the possible ways to create a wrapper is using the trait mixin and abstract override techniques. Scala has a sophisticated mechanism of dynamic class composition (inheritance, technically speaking) that allows to build a class from pieces (traits). While abstract override allows a trait to extend another trait and to provide the implementation of a method in the way that it will be able to call the implementation from its parent.\nMechanism explanation\rI know that the description above might sound like gibberish, so let me explain it with an example. // Our interface trait Printer { def print(): Unit } // A trait that extends Printer and overrides print // in the way that it makes a call to a parent class in its implementation // without any knowledge of what parent it is going to be. // And because there\u0026#39;s no parent known, `abstract` keyword is needed. trait PrinterButCooler extends Printer { abstract override def print(): Unit = { printf(\u0026#34;Hello \u0026#34;) super.print() printf(\u0026#34;!\u0026#34;) } } // The actual implementation of Printer trait class PrinterImpl() extends Printer { override def print() = printf(\u0026#34;World\u0026#34;) } new PrinterImpl().print() // Output: World // Now, to add additional functionality to PrinterImpl // we instantiate PrinterImpl and mix it with PrinterButCooler // but at the same time the class PrinterImpl is not aware of PrinterButCooler // as it\u0026#39;s only added at the moment of instantiation. (new PrinterImpl() with PrinterButCooler).print() // Output: Hello World! // Using this mechanism, it\u0026#39;s possible to compose an implementation with many trait mixins, // but the order of mixins is important as it defines the order of method calls. _ Let\u0026rsquo;s introduce a simple trait - ItemDao. trait ItemDao { def upsert(item: Item): Unit def get(id: Id): Option[Item] } As an example, we\u0026rsquo;ll implement mixins for ItemDao logging. To do that we need to create a trait that extends ItemDao and override its methods with abstract modifier (see an explanation in \u0026ldquo;Mechanism explanation\u0026rdquo; section). trait LoggedItemDaoWrapper extends ItemDao with StrictLogging { abstract override def upsert(item: Item): Unit = { logger.info(s\u0026#34;upsert($item) is called\u0026#34;) super.upsert(item) logger.info(s\u0026#34;upsert($item) = ()\u0026#34;) } abstract override def get(id: Id): Option[Item] = { logger.info(s\u0026#34;get($id) is called\u0026#34;) val result = super.get(id) logger.info(s\u0026#34;get($id) = $result\u0026#34;) } } Then, we should initialize a class that implements ItemDao interface and add the wrapper we made into it. new ItemDaoImpl(...) extends LoggedItemDaoWrapper So far it looks clean and seems easy to understand. You might be even eager to try it in your project, but unfortunately, it\u0026rsquo;s not that simple. Code is going to become messy if there are dependencies to provide. To show that, let\u0026rsquo;s make our trait return Future values: trait ItemDao { def upsert(item: Item): Future[Unit] def get(id: Id): Future[Option[Item]] } If a trait has methods that return Future, then an ExecutionContext must be provided. Otherwise, we won\u0026rsquo;t be able to call map, flatMap, and other methods of Future in the logic of our wrappers.\nThe most common workaround for this I have seen is to create a global single thread execution context. But what if it\u0026rsquo;s not an option? Then we have to provide ExecutionContext through the override def mechanism.\ntrait LoggedItemDaoWrapper extends ItemDao with StrictLogging { protected implicit def ec: ExecutionContext abstract override def upsert(item: Item): Future[Unit] = { super.upsert(item).map(_ =\u0026gt; logger.info(s\u0026#34;upsert($item) = ()\u0026#34;)) } abstract override def get(id: Id): Future[Option[Item]] = { super.get(id).map(result =\u0026gt; logger.info(s\u0026#34;get($id) = $result\u0026#34;)) } } val itemDao: ItemDao = new ItemDaoImpl(...) extends LoggedItemDaoWrapper { override def ec = yourEc } The code of initialization is not as clear as before because ExecutionContext has to be provided through the override def mechanism. An initialization order has to be kept in mind as it\u0026rsquo;s possible to get NullPointerException.\nThe more dependencies are stacked together the worse it gets.\nIt becomes even worse with Tagless Final. When we write our wrappers with TF in a project, we can\u0026rsquo;t do much with F[_] unless we have instances of typeclasses in a scope together with it. It would be reasonable to assume that each wrapper works with different set of type classes. So, when it comes to initialization, we\u0026rsquo;re doomed because now we need to provide all the dependencies for each wrapper.\nHere\u0026rsquo;s an example of what TF code might look like: trait ItemDao[F[_]] { def upsert(item: Item): F[Unit] def get(id: Id): F[Option[Item]] } trait LoggedItemDaoWrapper[F[_]] extends ItemDao[F] with StrictLogging { implicit protected def mt: MonadThrow[F] abstract override def upsert(item: Item): F[Unit] = { super.upsert(item).attemptTap(...) } abstract override def get(id: Id): F[Option[Item]] = { super.get(id).attemptTap(...) } } val itemDao: ItemDao[F] = new ItemDaoImpl[F](...) extends LoggedItemDaoWrapper[F] with MeteredItemDaoWrapper[F] with TimeoutItemDaoWrapper[F] { override def mt: MonadThrow[F] = async override def at: ApplicativeError[F] = async override def concurrent: Concurrent[F] = async override def timer: Timer[F] = _timer override def gauge: Gauge = methodCallGauge } Does it look ugly to you? It does to me. However, I would lie if I said there\u0026rsquo;s nothing we can do about it. The workaround we can apply to reduce the amount of boilerplate, is to create provider traits that will provide all the necessary dependencies for wrappers.\ntrait MonadThrowProvider[F[_]] extends ApplicativeThrowProvider[F] { implicit protected def mt: MonadThrow[F] override implicit protected def at: ApplicativeError[F] = mt } trait LoggingProvider[F[_]] extends MonadThrowProvider[F] with StrictLogging trait LoggedItemDaoWrapper[F[_]] extends ItemDao[F] with LoggingProvider[F] { abstract override def upsert(item: Item): F[Unit] = { super.upsert(item).attemptTap(...) } abstract override def get(id: Id): F[Option[Item]] = { super.get(id).attemptTap(...) } } As we\u0026rsquo;re already in this rabbit hole of blasphemy, we can go even further. Composing all providers into an all-in-one provider might be a good idea to reduce boilerplate in the initialization code. trait AllInOneProvider[F[_]] extends MonadThrowProvider[F] with ConcurrentProvider[F] with TimerProvider[F] with ClockProvider[F] with PrometheusGaugeProvider { override def mt: MonadThrow[F] = async override def concurrent: Concurrent[F] = async override def timer: Timer[F] = _timer override def clock: Clock[F] = _clock override def gauge: Gauge = methodCallGauge } // now with all-in-one provider the initialization code is quite clean val itemDao: ItemDao[F] = new ItemDaoImpl[F](...) extends LoggedItemDaoWrapper[F] with MeteredItemDaoWrapper[F] with TimeoutItemDaoWrapper[F] with AllInOneProvider[F] Finally, we have managed to hide all the ugly stuff behind AllInOneProvider trait and provider traits, but it\u0026rsquo;s hard to track the provided dependencies and their initialization.\nYes, it looks clean, but the code now smells even more. As this dependency provision is very exotic and completely unmaintainable. Not only that but such encoding of dependencies is very unnatural for Tagless Final and functional programming in general.\nClassic wrapper class Who would have thought that the classic way of doing things is the best way? The way that was designed for OOP languages works incredibly well in Scala with both Tagless Final and Scala Future. But I\u0026rsquo;m getting ahead of myself. Let\u0026rsquo;s introduce the same logging wrapper but using a classic wrapper class. And let\u0026rsquo;s do it for plain values, then for Future and finally for F[_].\nclass LoggedItemDaoWrapper(itemDao: ItemDao) extends ItemDao with StrictLogging { override def upsert(item: Item): Unit = { logger.info(s\u0026#34;upsert($item) is called\u0026#34;) itemDao.upsert(item) logger.info(s\u0026#34;upsert($item) = ()\u0026#34;) } override def get(id: Id): Option[Item] = { logger.info(s\u0026#34;get($id) is called\u0026#34;) val result = itemDao.get(id) logger.info(s\u0026#34;get($id) = $result\u0026#34;) } } class LoggedItemDaoWrapper(itemDao: ItemDao)(implicit ec: ExecutionContext) extends ItemDao with StrictLogging { override def upsert(item: Item): Future[Unit] = { itemDao.upsert(item).map(_ =\u0026gt; logger.info(s\u0026#34;upsert($item) = ()\u0026#34;)) } override def get(id: Id): Future[Option[Item]] = { super.get(id).map(result =\u0026gt; logger.info(s\u0026#34;get($id) = $result\u0026#34;)) } } class LoggedItemDaoWrapper[F[_]: MonadThrow](itemDao: ItemDao[F]) extends ItemDao[F] with StrictLogging { override def upsert(item: Item): F[Unit] = { itemDao.upsert(item).attemptTap(...) } override def get(id: Id): F[Option[Item]] = { itemDao.get(id).attemptTap(...) } } Looks nice! Easy to read and everything looks as idiomatic as it gets. But what about initialization?\nWell, having constructor parameters and implicit parameters reduces boilerplate significantly. With composition instead of inheritance we can easily wrap our implementation with as many wrappers as we want. It\u0026rsquo;s now clear which dependencies are used and to where they are passed.\nval itemDao: ItemDao[F] = new TimeoutItemDaoWrapper[F](timeoutsConfig)( new MeteredItemDaoWrapper[F](gauge)( new LoggedItemDaoWrapper[F]( new ItemDaoImpl[F](...) ) ) ) Syntactic sugar for class wrappers OOP wrappers make code look nice and tidy, but there are a few minor inconveniences. We wrap our implementation with wrappers and wrapper names now appear from the outermost wrapper to the innermost one. The other inconvinience is that we get nested code with probably the beefiest initialization code (ItemDaoImpl) in the innermost part.\nWith the power of Scala implicits, it is pretty easy to solve these issues.\nimplicit class WrapperHelper[A](private val a: A) extends AnyVal { def `with`[B \u0026gt;: A](wrap: A =\u0026gt; B): B = wrap(a) } \\\nTo achieve the best readability, we should add a companion object with apply function to our wrappers and have dependencies listed before to-be-wrapped class. On top of that we should separate dependencies and an implementation class with curring. Like this: class Wrapper[F[_]: TC1: TC2](dep1: Dep1, dep2: Dep2[F], o: MyClass[F]) extends MyClass[F] { ... } object Wrapper { def apply[F[_]: TC1: TC2](dep1: Dep1, dep2: Dep2[F])(o: MyClass[F]) = new Wrapper(dep1, dep2, o) } So the initialization code will look like this: myClassImpl .`with`(Wrapper1(dep1, dep2)) .`with`(Wrapper2(dep3))\nGame-changing distinction between inheritance-based wrapping (trait mixins) and composition-based wrapping (classic wrapper class) This distinction can be both very useful and very harmful, depending on the use case. The fact of the matter is that inheritance-based wrapping is able to call wrapped methods inside the implementation, while composition-based wrapping is not able to do so. So if you have an implementation that calls its own public methods inside, then with inheritance-based wrapping these internal calls will be wrapped as well.\nBelow, I wrote an example that will highlight described distinction.\nCode example\rtrait Printer { def print(): Unit def threeTimesPrint(): Unit } trait LoggedPrinter extends Printer { abstract override def print(): Unit = { println(\u0026#34;Print method is called\u0026#34;) super.print() } abstract override def threeTimesPrint(): Unit = { println(\u0026#34;ThreeTimesPrint method is called\u0026#34;) super.threeTimesPrint() } } class LoggedPrinter2(printer: Printer) extends Printer { override def print(): Unit = { println(\u0026#34;Print method is called\u0026#34;) printer.print() } override def threeTimesPrint(): Unit = { println(\u0026#34;ThreeTimesPrint method is called\u0026#34;) printer.threeTimesPrint() } } class PrinterImpl() extends Printer { override def print() = println(\u0026#34;A\u0026#34;) override def threeTimesPrint() = 1.to(3).foreach(_ =\u0026gt; print()) } // Output: // ThreeTimesPrint method is called // Print method is called // A // Print method is called // A // Print method is called // A (new PrinterImpl() with LoggedPrinter).threeTimesPrint() // Output: // ThreeTimesPrint method is called // A // A // A (new LoggedPrinter2(new PrinterImpl())).threeTimesPrint() Pros and Cons At the end of the article, it might seem that choice is clear, however after digging into details, many limitations and downsides are found.\nTrait mixin Pros:\n+ Preserves implementation type after wrapping. new ItemDaoImpl(...) extends LoggedItemDao has type ItemDaoImpl with LoggedItemDao. So it is possible to use any methods from ItemDaoImpl.\n+ With mixin traits it\u0026rsquo;s possible omit the methods that don\u0026rsquo;t need modification of their behavior. If we have a trait with 10 methods but want to add logging to one of them, then only one abstract override of the method needs to be written in a mixin.\n+ Wrapped version of a method is invoked on an inner call.\nCons:\n- Providing dependencies creates a lot of boilerplate.\n- Wrapping uses an inheritance mechanism. The order of initialization may not be clear.\n- Might lead to NPEs during initialization.\n- Looks ugly with Tagless Final.\n- Scala compiler doesn\u0026rsquo;t provide any warnings or errors if a method is not overridden in a wrapper.\n- Wrapped version of a method is invoked on an inner call. (Might be a downside in some cases)\nClassic wrapper class Pros:\n+ Easy-to-understand GOF pattern from OOP languages.\n+ Doesn\u0026rsquo;t get complicated no matter how many wrappers are composed.\n+ It\u0026rsquo;s possible to simplify initialization even more with syntactic sugar.\n+ Easy to use with Tagless Final.\n+ Scala compiler provides errors if a method is not overridden in a wrapper.\n+ When public method is called in an implementation internally, then none of the wrappers will be used.\nCons:\n- All the methods of a trait have to be overridden in a wrapper.\n- StrictLogging gets wrapper class instead of implementation by default.\n- A wrapper loses implementation type after wrapping making it impossible to call methods specific to the implementation.\n- When public method is called in an implementation internally, then none of the wrappers will be used.\nConclusion In my experience, the classic wrapper classes are the way to go. They are easy to understand, easy to use, and easy to maintain. I\u0026rsquo;m yet to see a case it\u0026rsquo;s not the best choice.\n","permalink":"https://awethon.github.io/posts/scala-wrappers/","summary":"\u003cp\u003eScala is a great language with many features and a complex type system.\nIt provides many ways to solve the same problem, but each way has its own pros and cons.\nUnfortunately, it\u0026rsquo;s a common problem of scala developers to utilize the features of Scala, disregarding the cognitive load that it might bring to the team. \u003cbr\u003e\nIn this article, I\u0026rsquo;ll try to advocate for a simple and easy-to-understand way of implementing wrappers in Scala.\u003c/p\u003e","title":"Different ways to implement wrappers in Scala"},{"content":"The remote development feature allows its users to have an IDE or a code editor server on a remote machine. All the local machine needs is a client that draws an interface, and sends, and receives data.\nThe industry is already pushing in this direction: VSCode was the first to introduce this feature. It\u0026rsquo;s much easier for VSCode since it\u0026rsquo;s a code editor built on electron (JS), while for example, IntelliJ IDEA is an IDE and uses Java Swing library for UI.\nIn Autumn 2022, JetBrains also caught this trend and announced a remote development feature for its products and a completely new IDE built from scratch that also supports this feature. GitHub has also introduced a remote development feature which allows using VSCode in a browser.\nRemote development is a fast-growing trend right now, and there are reasons for it. I\u0026rsquo;ve tried it with IntelliJ IDEA and was inspired by the number of advantages remote development gives. Even though this feature is still in beta and bugs appear from time to time, it\u0026rsquo;s totally worth it to try.\nLet\u0026rsquo;s now look at all the advantages remote development gives to software developers and companies.\nNo need for powerful laptops anymore No need to upgrade employees\u0026rsquo; laptops every 2-3 years. Cheaper alternatives, such as an iPad with a keyboard instead of a MacBook Pro, could be bought. Even Raspberry Pi could be used!\nNot only does it reduce the costs for a company but also to access an IDE from any device with a browser which is a huge advantage.\nLonger battery life for a laptop As all computations are done on a remote machine, the laptop\u0026rsquo;s CPU and RAM are not used that much. It means that the battery life of a laptop is increased, making it possible to work for a longer time without charging.\nThe same environment for every developer Did you encounter a problem where different people were running the same code, but it only worked one of them? It won\u0026rsquo;t happen anymore! As all the code will run in the same environment, the problem will be solved.\nCross-platform solution Developers won\u0026rsquo;t be limited in choosing preferable devices anymore.\nSoftware development using iPad with a keyboard? Not a problem anymore!\nA PC on Windows will become a viable option too.\nReduction of costs by using cloud infrastructure Big tech companies are able to use the advantages of cloud infrastructure.\nThey can use the same virtual machine for multiple developers, reducing the costs of maintaining a lot of powerful laptops.\nCode is not stored on disk locally The risk of code being stolen is reduced. In case of a laptop being stolen, all that has to be done is changing the password.\nLocality of data Big tech companies might have their own docker registry or artifact repository like Artifactory. Having your virtual machine in the same data center would mean almost instant download and upload of the libraries and docker images.\nFaster onboarding Since everything is stored on a virtual machine, the environment for new developers can be prepared in advance to reduce onboarding time significantly.\nIt is very interesting to see how the industry is moving toward remote development. The only disadvantage I see is the inability to work without the Internet, but the mobile internet is available almost everywhere nowadays. Hope that you\u0026rsquo;ll like the new era of remote development too!\nLinks: VSCode Remote Development\nRemote Development for JetBrains products\nJetBrains Remote Development announcement post\nGitHub Codespaces\n","permalink":"https://awethon.github.io/posts/remote-development-is-future/","summary":"\u003cp\u003eThe remote development feature allows its users to have an IDE or a code editor server on a remote machine.\nAll the local machine needs is a client that draws an interface, and sends, and receives data.\u003c/p\u003e\n\u003cp\u003eThe industry is already pushing in this direction: VSCode was the first to introduce this feature.\nIt\u0026rsquo;s much easier for VSCode since it\u0026rsquo;s a code editor built on electron (JS), while for example, IntelliJ IDEA is an IDE and uses Java Swing library for UI.\u003cbr\u003e\nIn Autumn 2022, JetBrains also caught this trend and announced a remote development feature for its products and a completely new IDE built from scratch that also supports this feature.\nGitHub has also introduced a remote development feature which allows using VSCode in a browser.\u003c/p\u003e","title":"Remote development is the future of big tech"}] \ No newline at end of file +[{"content":"Scala is a great language with many features and a complex type system. It provides many ways to solve the same problem, but each way has its own pros and cons. Unfortunately, it\u0026rsquo;s a common problem of scala developers to utilize the features of Scala, disregarding the cognitive load that it might bring to the team. In this article, I\u0026rsquo;ll try to advocate for a simple and easy-to-understand way of implementing wrappers in Scala.\nBut what are these wrappers and what are they good for? The wrapper pattern (or the proxy pattern) is a design pattern that allows to add new functionality to an existing class without altering it. It is useful in scenarios where an application needs to be instrumented, meaning it needs to be monitored, metered, and traced. Putting all the instrumentation in business logic will make code less readable by mixing business logic with monitoring code. And that\u0026rsquo;s where wrappers shine. However, they can be used for other purposes as well (like timeouts and retries). (NB: transforming an argument or a return value in a wrapper is considered to be an anti-pattern.)\nThere are two main ways to implement wrappers in Scala: using trait mixins and using classic wrapper classes. In previous edition this article covered a third way of implementing wrappers in Scala using Mid type class from tofu library. But it had many disadvantages and tofu is not maintained anymore. So, I decided to remove it from the article. For some reason trait mixins were widely used in Scala community and in the past using mixins was considered to be \u0026ldquo;The Scala Way\u0026rdquo;. And I still see developers using them. However, I don\u0026rsquo;t think that using mixins is a good idea in most cases. I\u0026rsquo;ll try to explain why in this article.\nTrait mixin One of the possible ways to create a wrapper is using the trait mixin and abstract override techniques. Scala has a sophisticated mechanism of dynamic class composition (inheritance, technically speaking) that allows to build a class from pieces (traits). While abstract override allows a trait to extend another trait and to provide the implementation of a method in the way that it will be able to call the implementation from its parent.\nMechanism explanation\rI know that the description above might sound like gibberish, so let me explain it with an example. // Our interface trait Printer { def print(): Unit } // A trait that extends Printer and overrides print // in the way that it makes a call to a parent class in its implementation // without any knowledge of what parent it is going to be. // And because there\u0026#39;s no parent known, `abstract` keyword is needed. trait PrinterButCooler extends Printer { abstract override def print(): Unit = { printf(\u0026#34;Hello \u0026#34;) super.print() printf(\u0026#34;!\u0026#34;) } } // The actual implementation of Printer trait class PrinterImpl() extends Printer { override def print() = printf(\u0026#34;World\u0026#34;) } new PrinterImpl().print() // Output: World // Now, to add additional functionality to PrinterImpl // we instantiate PrinterImpl and mix it with PrinterButCooler // but at the same time the class PrinterImpl is not aware of PrinterButCooler // as it\u0026#39;s only added at the moment of instantiation. (new PrinterImpl() with PrinterButCooler).print() // Output: Hello World! // Using this mechanism, it\u0026#39;s possible to compose an implementation with many trait mixins, // but the order of mixins is important as it defines the order of method calls. _ Let\u0026rsquo;s introduce a simple trait - ItemDao. trait ItemDao { def upsert(item: Item): Unit def get(id: Id): Option[Item] } As an example, we\u0026rsquo;ll implement mixins for ItemDao logging. To do that we need to create a trait that extends ItemDao and override its methods with abstract modifier (see an explanation in \u0026ldquo;Mechanism explanation\u0026rdquo; section). trait LoggedItemDaoWrapper extends ItemDao with StrictLogging { abstract override def upsert(item: Item): Unit = { logger.info(s\u0026#34;upsert($item) is called\u0026#34;) super.upsert(item) logger.info(s\u0026#34;upsert($item) = ()\u0026#34;) } abstract override def get(id: Id): Option[Item] = { logger.info(s\u0026#34;get($id) is called\u0026#34;) val result = super.get(id) logger.info(s\u0026#34;get($id) = $result\u0026#34;) } } Then, we should initialize a class that implements ItemDao interface and add the wrapper we made into it. new ItemDaoImpl(...) extends LoggedItemDaoWrapper So far it looks clean and seems easy to understand. You might be even eager to try it in your project, but unfortunately, it\u0026rsquo;s not that simple. Code is going to become messy if there are dependencies to provide. To show that, let\u0026rsquo;s make our trait return Future values: trait ItemDao { def upsert(item: Item): Future[Unit] def get(id: Id): Future[Option[Item]] } If a trait has methods that return Future, then an ExecutionContext must be provided. Otherwise, we won\u0026rsquo;t be able to call map, flatMap, and other methods of Future in the logic of our wrappers.\nThe most common workaround for this I have seen is to create a global single thread execution context. But what if it\u0026rsquo;s not an option? Then we have to provide ExecutionContext through the override def mechanism.\ntrait LoggedItemDaoWrapper extends ItemDao with StrictLogging { protected implicit def ec: ExecutionContext abstract override def upsert(item: Item): Future[Unit] = { super.upsert(item).map(_ =\u0026gt; logger.info(s\u0026#34;upsert($item) = ()\u0026#34;)) } abstract override def get(id: Id): Future[Option[Item]] = { super.get(id).map(result =\u0026gt; logger.info(s\u0026#34;get($id) = $result\u0026#34;)) } } val itemDao: ItemDao = new ItemDaoImpl(...) extends LoggedItemDaoWrapper { override def ec = yourEc } The code of initialization is not as clear as before because ExecutionContext has to be provided through the override def mechanism. An initialization order has to be kept in mind as it\u0026rsquo;s possible to get NullPointerException.\nThe more dependencies are stacked together the worse it gets.\nIt becomes even worse with Tagless Final. When we write our wrappers with TF in a project, we can\u0026rsquo;t do much with F[_] unless we have instances of typeclasses in a scope together with it. It would be reasonable to assume that each wrapper works with different set of type classes. So, when it comes to initialization, we\u0026rsquo;re doomed because now we need to provide all the dependencies for each wrapper.\nHere\u0026rsquo;s an example of what TF code might look like: trait ItemDao[F[_]] { def upsert(item: Item): F[Unit] def get(id: Id): F[Option[Item]] } trait LoggedItemDaoWrapper[F[_]] extends ItemDao[F] with StrictLogging { implicit protected def mt: MonadThrow[F] abstract override def upsert(item: Item): F[Unit] = { super.upsert(item).attemptTap(...) } abstract override def get(id: Id): F[Option[Item]] = { super.get(id).attemptTap(...) } } val itemDao: ItemDao[F] = new ItemDaoImpl[F](...) extends LoggedItemDaoWrapper[F] with MeteredItemDaoWrapper[F] with TimeoutItemDaoWrapper[F] { override def mt: MonadThrow[F] = async override def at: ApplicativeError[F] = async override def concurrent: Concurrent[F] = async override def timer: Timer[F] = _timer override def gauge: Gauge = methodCallGauge } Does it look ugly to you? It does to me. However, I would lie if I said there\u0026rsquo;s nothing we can do about it. The workaround we can apply to reduce the amount of boilerplate, is to create provider traits that will provide all the necessary dependencies for wrappers.\ntrait MonadThrowProvider[F[_]] extends ApplicativeThrowProvider[F] { implicit protected def mt: MonadThrow[F] override implicit protected def at: ApplicativeError[F] = mt } trait LoggingProvider[F[_]] extends MonadThrowProvider[F] with StrictLogging trait LoggedItemDaoWrapper[F[_]] extends ItemDao[F] with LoggingProvider[F] { abstract override def upsert(item: Item): F[Unit] = { super.upsert(item).attemptTap(...) } abstract override def get(id: Id): F[Option[Item]] = { super.get(id).attemptTap(...) } } As we\u0026rsquo;re already in this rabbit hole of blasphemy, we can go even further. Composing all providers into an all-in-one provider might be a good idea to reduce boilerplate in the initialization code. trait AllInOneProvider[F[_]] extends MonadThrowProvider[F] with ConcurrentProvider[F] with TimerProvider[F] with ClockProvider[F] with PrometheusGaugeProvider { override def mt: MonadThrow[F] = async override def concurrent: Concurrent[F] = async override def timer: Timer[F] = _timer override def clock: Clock[F] = _clock override def gauge: Gauge = methodCallGauge } // now with all-in-one provider the initialization code is quite clean val itemDao: ItemDao[F] = new ItemDaoImpl[F](...) extends LoggedItemDaoWrapper[F] with MeteredItemDaoWrapper[F] with TimeoutItemDaoWrapper[F] with AllInOneProvider[F] Finally, we have managed to hide all the ugly stuff behind AllInOneProvider trait and provider traits, but it\u0026rsquo;s hard to track the provided dependencies and their initialization.\nYes, it looks clean, but the code now smells even more. As this dependency provision is very exotic and completely unmaintainable. Not only that but such encoding of dependencies is very unnatural for Tagless Final and functional programming in general.\nClassic wrapper class Who would have thought that the classic way of doing things is the best way? The way that was designed for OOP languages works incredibly well in Scala with both Tagless Final and Scala Future. But I\u0026rsquo;m getting ahead of myself. Let\u0026rsquo;s introduce the same logging wrapper but using a classic wrapper class. And let\u0026rsquo;s do it for plain values, then for Future and finally for F[_].\nclass LoggedItemDaoWrapper(itemDao: ItemDao) extends ItemDao with StrictLogging { override def upsert(item: Item): Unit = { logger.info(s\u0026#34;upsert($item) is called\u0026#34;) itemDao.upsert(item) logger.info(s\u0026#34;upsert($item) = ()\u0026#34;) } override def get(id: Id): Option[Item] = { logger.info(s\u0026#34;get($id) is called\u0026#34;) val result = itemDao.get(id) logger.info(s\u0026#34;get($id) = $result\u0026#34;) } } class LoggedItemDaoWrapper(itemDao: ItemDao)(implicit ec: ExecutionContext) extends ItemDao with StrictLogging { override def upsert(item: Item): Future[Unit] = { itemDao.upsert(item).map(_ =\u0026gt; logger.info(s\u0026#34;upsert($item) = ()\u0026#34;)) } override def get(id: Id): Future[Option[Item]] = { super.get(id).map(result =\u0026gt; logger.info(s\u0026#34;get($id) = $result\u0026#34;)) } } class LoggedItemDaoWrapper[F[_]: MonadThrow](itemDao: ItemDao[F]) extends ItemDao[F] with StrictLogging { override def upsert(item: Item): F[Unit] = { itemDao.upsert(item).attemptTap(...) } override def get(id: Id): F[Option[Item]] = { itemDao.get(id).attemptTap(...) } } Looks nice! Easy to read and everything looks as idiomatic as it gets. But what about initialization?\nWell, having constructor parameters and implicit parameters reduces boilerplate significantly. With composition instead of inheritance we can easily wrap our implementation with as many wrappers as we want. It\u0026rsquo;s now clear which dependencies are used and to where they are passed.\nval itemDao: ItemDao[F] = new TimeoutItemDaoWrapper[F](timeoutsConfig)( new MeteredItemDaoWrapper[F](gauge)( new LoggedItemDaoWrapper[F]( new ItemDaoImpl[F](...) ) ) ) Syntactic sugar for class wrappers OOP wrappers make code look nice and tidy, but there are a few minor inconveniences. We wrap our implementation with wrappers and wrapper names now appear from the outermost wrapper to the innermost one. The other inconvinience is that we get nested code with probably the beefiest initialization code (ItemDaoImpl) in the innermost part.\nWith the power of Scala implicits, it is pretty easy to solve these issues.\nimplicit class WrapperHelper[A](private val a: A) extends AnyVal { def `with`[B \u0026gt;: A](wrap: A =\u0026gt; B): B = wrap(a) } \\\nTo achieve the best readability, we should add a companion object with apply function to our wrappers and have dependencies listed before to-be-wrapped class. On top of that we should separate dependencies and an implementation class with curring. Like this: class Wrapper[F[_]: TC1: TC2](dep1: Dep1, dep2: Dep2[F], o: MyClass[F]) extends MyClass[F] { ... } object Wrapper { def apply[F[_]: TC1: TC2](dep1: Dep1, dep2: Dep2[F])(o: MyClass[F]) = new Wrapper(dep1, dep2, o) } So the initialization code will look like this: myClassImpl .`with`(Wrapper1(dep1, dep2)) .`with`(Wrapper2(dep3))\nGame-changing distinction between inheritance-based wrapping (trait mixins) and composition-based wrapping (classic wrapper class) This distinction can be both very useful and very harmful, depending on the use case. The fact of the matter is that inheritance-based wrapping is able to call wrapped methods inside the implementation, while composition-based wrapping is not able to do so. So if you have an implementation that calls its own public methods inside, then with inheritance-based wrapping these internal calls will be wrapped as well.\nBelow, I wrote an example that will highlight described distinction.\nCode example\rtrait Printer { def print(): Unit def threeTimesPrint(): Unit } trait LoggedPrinter extends Printer { abstract override def print(): Unit = { println(\u0026#34;Print method is called\u0026#34;) super.print() } abstract override def threeTimesPrint(): Unit = { println(\u0026#34;ThreeTimesPrint method is called\u0026#34;) super.threeTimesPrint() } } class LoggedPrinter2(printer: Printer) extends Printer { override def print(): Unit = { println(\u0026#34;Print method is called\u0026#34;) printer.print() } override def threeTimesPrint(): Unit = { println(\u0026#34;ThreeTimesPrint method is called\u0026#34;) printer.threeTimesPrint() } } class PrinterImpl() extends Printer { override def print() = println(\u0026#34;A\u0026#34;) override def threeTimesPrint() = 1.to(3).foreach(_ =\u0026gt; print()) } // Output: // ThreeTimesPrint method is called // Print method is called // A // Print method is called // A // Print method is called // A (new PrinterImpl() with LoggedPrinter).threeTimesPrint() // Output: // ThreeTimesPrint method is called // A // A // A (new LoggedPrinter2(new PrinterImpl())).threeTimesPrint() Pros and Cons At the end of the article, it might seem that choice is clear, however after digging into details, many limitations and downsides are found.\nTrait mixin Pros:\n+ Preserves implementation type after wrapping. new ItemDaoImpl(...) extends LoggedItemDao has type ItemDaoImpl with LoggedItemDao. So it is possible to use any methods from ItemDaoImpl.\n+ With mixin traits it\u0026rsquo;s possible omit the methods that don\u0026rsquo;t need modification of their behavior. If we have a trait with 10 methods but want to add logging to one of them, then only one abstract override of the method needs to be written in a mixin.\n+ Wrapped version of a method is invoked on an inner call.\nCons:\n- Providing dependencies creates a lot of boilerplate.\n- Wrapping uses an inheritance mechanism. The order of initialization may not be clear.\n- Might lead to NPEs during initialization.\n- Looks ugly with Tagless Final.\n- Scala compiler doesn\u0026rsquo;t provide any warnings or errors if a method is not overridden in a wrapper.\n- Wrapped version of a method is invoked on an inner call. (Might be a downside in some cases)\nClassic wrapper class Pros:\n+ Easy-to-understand GOF pattern from OOP languages.\n+ Doesn\u0026rsquo;t get complicated no matter how many wrappers are composed.\n+ It\u0026rsquo;s possible to simplify initialization even more with syntactic sugar.\n+ Easy to use with Tagless Final.\n+ Scala compiler provides errors if a method is not overridden in a wrapper.\n+ When public method is called in an implementation internally, then none of the wrappers will be used.\nCons:\n- All the methods of a trait have to be overridden in a wrapper.\n- StrictLogging gets wrapper class instead of implementation by default.\n- A wrapper loses implementation type after wrapping making it impossible to call methods specific to the implementation.\n- When public method is called in an implementation internally, then none of the wrappers will be used.\nConclusion In my experience, the classic wrapper classes are the way to go. They are easy to understand, easy to use, and easy to maintain. I\u0026rsquo;m yet to see a case where it\u0026rsquo;s not the best choice.\n","permalink":"https://awethon.github.io/posts/scala-wrappers/","summary":"\u003cp\u003eScala is a great language with many features and a complex type system.\nIt provides many ways to solve the same problem, but each way has its own pros and cons.\nUnfortunately, it\u0026rsquo;s a common problem of scala developers to utilize the features of Scala, disregarding the cognitive load that it might bring to the team. \u003cbr\u003e\nIn this article, I\u0026rsquo;ll try to advocate for a simple and easy-to-understand way of implementing wrappers in Scala.\u003c/p\u003e","title":"Different ways to implement wrappers in Scala"},{"content":"The remote development feature allows its users to have an IDE or a code editor server on a remote machine. All the local machine needs is a client that draws an interface, and sends, and receives data.\nThe industry is already pushing in this direction: VSCode was the first to introduce this feature. It\u0026rsquo;s much easier for VSCode since it\u0026rsquo;s a code editor built on electron (JS), while for example, IntelliJ IDEA is an IDE and uses Java Swing library for UI.\nIn Autumn 2022, JetBrains also caught this trend and announced a remote development feature for its products and a completely new IDE built from scratch that also supports this feature. GitHub has also introduced a remote development feature which allows using VSCode in a browser.\nRemote development is a fast-growing trend right now, and there are reasons for it. I\u0026rsquo;ve tried it with IntelliJ IDEA and was inspired by the number of advantages remote development gives. Even though this feature is still in beta and bugs appear from time to time, it\u0026rsquo;s totally worth it to try.\nLet\u0026rsquo;s now look at all the advantages remote development gives to software developers and companies.\nNo need for powerful laptops anymore No need to upgrade employees\u0026rsquo; laptops every 2-3 years. Cheaper alternatives, such as an iPad with a keyboard instead of a MacBook Pro, could be bought. Even Raspberry Pi could be used!\nNot only does it reduce the costs for a company but also to access an IDE from any device with a browser which is a huge advantage.\nLonger battery life for a laptop As all computations are done on a remote machine, the laptop\u0026rsquo;s CPU and RAM are not used that much. It means that the battery life of a laptop is increased, making it possible to work for a longer time without charging.\nThe same environment for every developer Did you encounter a problem where different people were running the same code, but it only worked one of them? It won\u0026rsquo;t happen anymore! As all the code will run in the same environment, the problem will be solved.\nCross-platform solution Developers won\u0026rsquo;t be limited in choosing preferable devices anymore.\nSoftware development using iPad with a keyboard? Not a problem anymore!\nA PC on Windows will become a viable option too.\nReduction of costs by using cloud infrastructure Big tech companies are able to use the advantages of cloud infrastructure.\nThey can use the same virtual machine for multiple developers, reducing the costs of maintaining a lot of powerful laptops.\nCode is not stored on disk locally The risk of code being stolen is reduced. In case of a laptop being stolen, all that has to be done is changing the password.\nLocality of data Big tech companies might have their own docker registry or artifact repository like Artifactory. Having your virtual machine in the same data center would mean almost instant download and upload of the libraries and docker images.\nFaster onboarding Since everything is stored on a virtual machine, the environment for new developers can be prepared in advance to reduce onboarding time significantly.\nIt is very interesting to see how the industry is moving toward remote development. The only disadvantage I see is the inability to work without the Internet, but the mobile internet is available almost everywhere nowadays. Hope that you\u0026rsquo;ll like the new era of remote development too!\nLinks: VSCode Remote Development\nRemote Development for JetBrains products\nJetBrains Remote Development announcement post\nGitHub Codespaces\n","permalink":"https://awethon.github.io/posts/remote-development-is-future/","summary":"\u003cp\u003eThe remote development feature allows its users to have an IDE or a code editor server on a remote machine.\nAll the local machine needs is a client that draws an interface, and sends, and receives data.\u003c/p\u003e\n\u003cp\u003eThe industry is already pushing in this direction: VSCode was the first to introduce this feature.\nIt\u0026rsquo;s much easier for VSCode since it\u0026rsquo;s a code editor built on electron (JS), while for example, IntelliJ IDEA is an IDE and uses Java Swing library for UI.\u003cbr\u003e\nIn Autumn 2022, JetBrains also caught this trend and announced a remote development feature for its products and a completely new IDE built from scratch that also supports this feature.\nGitHub has also introduced a remote development feature which allows using VSCode in a browser.\u003c/p\u003e","title":"Remote development is the future of big tech"}] \ No newline at end of file diff --git a/posts/scala-wrappers/index.html b/posts/scala-wrappers/index.html index 814a0eb..64ed845 100644 --- a/posts/scala-wrappers/index.html +++ b/posts/scala-wrappers/index.html @@ -8,7 +8,7 @@ In this article, I’ll try to advocate for a simple and easy-to-understand way of implementing wrappers in Scala.">

Different ways to implement wrappers in Scala

Scala is a great language with many features and a complex type system. +In this article, I’ll try to advocate for a simple and easy-to-understand way of implementing wrappers in Scala.">

Different ways to implement wrappers in Scala

Scala is a great language with many features and a complex type system. It provides many ways to solve the same problem, but each way has its own pros and cons. Unfortunately, it’s a common problem of scala developers to utilize the features of Scala, disregarding the cognitive load that it might bring to the team.
In this article, I’ll try to advocate for a simple and easy-to-understand way of implementing wrappers in Scala.

But what are these wrappers and what are they good for?
The wrapper pattern (or the proxy pattern) is a design pattern that allows to add new functionality to an existing class without altering it. It is useful in scenarios where an application needs to be instrumented, meaning it needs to be monitored, metered, and traced. @@ -264,7 +264,7 @@ // A (new LoggedPrinter2(new PrinterImpl())).threeTimesPrint()


Pros and Cons

At the end of the article, it might seem that choice is clear, however after digging into details, many limitations and downsides are found.

Trait mixin

Pros:
+ Preserves implementation type after wrapping. new ItemDaoImpl(...) extends LoggedItemDao has type ItemDaoImpl with LoggedItemDao. So it is possible to use any methods from ItemDaoImpl.
+ With mixin traits it’s possible omit the methods that don’t need modification of their behavior. If we have a trait with 10 methods but want to add logging to one of them, then only one abstract override of the method needs to be written in a mixin.
+ Wrapped version of a method is invoked on an inner call.

Cons:
- Providing dependencies creates a lot of boilerplate.
- Wrapping uses an inheritance mechanism. The order of initialization may not be clear.
- Might lead to NPEs during initialization.
- Looks ugly with Tagless Final.
- Scala compiler doesn’t provide any warnings or errors if a method is not overridden in a wrapper.
- Wrapped version of a method is invoked on an inner call. (Might be a downside in some cases)

Classic wrapper class

Pros:
+ Easy-to-understand GOF pattern from OOP languages.
+ Doesn’t get complicated no matter how many wrappers are composed.
+ It’s possible to simplify initialization even more with syntactic sugar.
+ Easy to use with Tagless Final.
+ Scala compiler provides errors if a method is not overridden in a wrapper.
+ When public method is called in an implementation internally, then none of the wrappers will be used.

Cons:
- All the methods of a trait have to be overridden in a wrapper.
- StrictLogging gets wrapper class instead of implementation by default.
- A wrapper loses implementation type after wrapping making it impossible to call methods specific to the implementation.
- When public method is called in an implementation internally, then none of the wrappers will be used.

Conclusion

In my experience, the classic wrapper classes are the way to go. They are easy to understand, easy to use, and easy to maintain. -I’m yet to see a case it’s not the best choice.