2
$\begingroup$

I just asked this question about Rust: Is it possible to create a default trait implementation in Rust, and then override that trait implementation somewhere else? My problem is, in my custom lang, I want to define a default implementation for printing error messages to the terminal. What I hate the most about JS/Node.js is the error message printing is terrible, so I would like to override all error messages and reformat it. Likewise in my language, I wanted to have the ability to override how error messages are displayed. And other things will use this pattern, but this is just top-of-mind.

The question is, how can I:

  1. Define the default implementation of stringifying?
  2. Allow overriding (possibly multiple times) of the stringification?

By multiple times, I mean a library might define some errors and renderings for them. Then another library might override that. Then an application might override that, so 3 layers of definitions, the last one which will take precedence (and all internal/external error messages will use that final formatter).

How can I do this in a statically typed sort of way? It appears Rust doesn't allow you to override trait implementations (for some reason I'm unaware of, waiting to hear about that!). Could you theoretically just add that feature to Rust without consequences? What about other languages, what is a good example how another language solves this problem?

In some ways it's kind of like dependency injection, but not 100% sure about that.

$\endgroup$
3
  • $\begingroup$ Is this similar to alias_method in Ruby? Could that be done in a statically typed sort of language somehow? $\endgroup$
    – Lance
    Jul 8, 2023 at 10:03
  • $\begingroup$ Yes, what you're describing is basically solved by dependency injection. Scala has given as one answer already mentions, which are more exactly like the "overridden trait implementation" solution you describe, but it also has implicit variables, which allow actual dependency injection too. I guess these both solve the same problem in parallel ways? I don't know enough about Scala to answer for sure though. $\endgroup$
    – pxeger
    Jul 8, 2023 at 12:19
  • $\begingroup$ Sooner or later you're going to want to "block overrides": override X with this Y, but don't let further code override Y. Somewhat like file permissions in NTFS. $\endgroup$
    – Pablo H
    Aug 10, 2023 at 14:57

2 Answers 2

5
$\begingroup$

Scala uses the given system for a superset of what Rust traits do. For example, this Rust (pseudo-)code

trait Show {
  fn show(&self) -> String;
}

impl Show for String {
  fn show(&self) -> String {
    format!("\"{}\"", self)
  }
}

fn show(x: &impl Show) {
  x.show()
}

converts to this Scala code:

trait Show[+A]:
  def show(a: A): String

given Show[String] where
  def show(str: String) = s"\"$str\""

def show[A](a: A)(using sh: Show[A]) = sh(a)

The important difference is that givens act as variables - that is, they can be scoped.

def foo() =
  given Show[String] where
    def show(str: String) = s"[${str.length}]$str"
  println(show("hi"))

println(show("hi")) // outputs "hi"
foo() // outputs [2]hi

Multiple libraries can export givens of the same type and you can choose which to use when you import them:

import lib.{given Show[String]}
println(show("hi")) // calls the library's implementation

And of course, you can mix the two things and use imports inside a block to override the default given

import bar.{given Show[String]}
def q() =
  import foo.{given Show[String]}
  println(show("hi"))
println(show("hi"))
q()

You can also decide to explicitly pass an instance to a function without having it as a given:

object A extends Show[String]:
  def show(str: String) = s"$str from A"
object B extends Show[String]:
  def show(str: String) = s"$str from B"
given Show[String] = A
println(show("hi")) // hi from A
println(show("hi")(using B)) // hi from B

All these features combined can make Scala givens much more powerful than Rust-style traits or traditional Haskell-like typeclasses, if used correctly.

$\endgroup$
3
  • $\begingroup$ I don't see how this would allow overriding the calls to show that were done inside some external library already. How could this system allow me to change what show does in places it's already been called at in external libraries? $\endgroup$
    – Lance
    Jul 8, 2023 at 10:05
  • 1
    $\begingroup$ @Lance sorry, didn't get that it was what your question is asking. If your library defines a given Show[String] and uses it internally - that is, doesn't expose it as an (optional) using parameter - I don't think there's a way to change that $\endgroup$
    – RubenVerg
    Jul 8, 2023 at 14:45
  • $\begingroup$ @Lance If external library X has a function with signature def foo[A: Show](argument: A), that's syntax sugar for def foo[A](argument: A)(using showInstance: Show[A]). And when that function calls show, it's really calling some method on that context object showInstance. That means that your calling library Y can invoke the function like foo(whatever) to get the "default" Show instance but can also invoke it as foo(whatever)(using customShowImpl) to force a custom one. $\endgroup$ Jul 8, 2023 at 16:02
2
$\begingroup$

Your traits calls library code

By multiple times, I mean a library might define some errors and renderings for them. Then another library might override that. Then an application might override that, so 3 layers of definitions, the last one which will take precedence (and all internal/external error messages will use that final formatter).

In existing languages, this level of configuration is possible if the default implementation is published in a public vtable, and default trait implementations calls into these vtables, that in turn are initially filled with the default implementation. Any library or user code can, then, change that. It's only a form of dependency pointer injection, as noted.

impl in Rust, is more in direction of dependency code injection, in comptime no less. After the code is compiled, there is only functionality, no extension points.

Aspect-oriented programming

So if you want both, your language will need some form of virtual trait that accepts a default implementation, so that any library or user code can modify latter. If these 3 layers of (re)definitions are to occur at comptime, then your language will need some syntax to declare some aspect syntax rewriting rules.

You will note, in above Wikipedia link, that logging is a prime case in aspect-oriented programming.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .