Why does the Scala API have two strategies for organizing types?

I noticed that the Scala standard library uses two different strategies for organizing classes, attributes, and singleton objects.

  • Using packages whose members are imported. This, for example, is how you access scala.collection.mutable.ListBuffer . This method is familiar with Java, Python, etc.

  • Using feature type elements. This, for example, is how you access the Parser type. First you need to mix in scala.util.parsing.combinator.Parsers . This method is not familiar with Java, Python, etc., and it is little used in third-party libraries.

I assume that one advantage of (2) is that it organizes both methods and types, but in light of the objects in the Scala 2.8 package, the same can be done with (1). Why both of these strategies? When should everyone be used?

+4
source share
2 answers

The nomenclature notes here are path dependent types . This is option number 2 that you are talking about, and I will only talk about it. If you do not have a problem solved by him, you should always choose option number 1.

What you will miss is that the Parser class refers to things defined in the Parsers class. In fact, the Parser class itself depends on what input was defined on Parsers :

 abstract class Parser[+T] extends (Input => ParseResult[T]) 

The input type is defined as follows:

 type Input = Reader[Elem] 

And Elem is abstract. Consider, for example, RegexParsers and TokenParsers . The former defines Elem as Char , and the latter defines it as Token . This means that Parser is different for everyone. More importantly, since Parser is a subclass of Parsers , the Scala compiler ensures that at compile time you do not pass RegexParsers Parser to TokenParsers or vice versa. In fact, you cannot even pass the Parser one instance of RegexParsers another instance.

+5
source

The second is also known as the cake template . The advantage is that the code inside the class that has the mixed attribute becomes independent of the concrete implementation of the methods and types in this attribute. It allows you to use the members of the trait without knowing what their specific implementation is.

 trait Logging { def log(msg: String) } trait App extends Logging { log("My app started.") } 

Above, the Logging attribute is a requirement for the App (requirements can also be expressed using self types). Then at some point in your application, you can decide what will be implemented and mix the implementation trait with a specific class.

 trait ConsoleLogging extends Logging { def log(msg: String) = println(msg) } object MyApp extends App with ConsoleLogging 

This has an advantage over import in the sense that the requirements of your piece of code are not related to the implementation defined by the import statement. In addition, it allows you to create and distribute an API that can be used in another assembly elsewhere, provided that its requirements are met by mixing in a particular implementation.

However, you must be careful when using this template.

  • All classes defined inside the attribute will have a reference to the outer class. This can be a performance issue, or when you use serialization (when the outer class is not serializable or, even worse, if it is, but you don't want it to be serialized).
  • If your β€œmodule” becomes really large, you will either have a very large tag, or a very large source file, or it will have to distribute the element code to multiple files. This may lead to some pattern.
  • This can make you write your entire application using this paradigm. Before you know this, each class will have to mix its requirements.
  • The specific implementation should be known at compile time if you are not using any handwritten delegation. The dynamic dynamic speaker cannot be mixed depending on the value available at runtime.

I think that the developers of the library did not consider any of the above issues that are about Parsers .

+4
source

Source: https://habr.com/ru/post/1343959/


All Articles