1. Introduction

The Scala language has undergone a complete overhaul, and a new version was released over a year ago as Scala 3. A lot of new features were introduced as part of the latest release.

In this tutorial, we’ll look at another new feature in Scala 3, the export clause.

2. Export Clause

The export clause defines an alias for an object’s members. It helps to expose selected members from a class without writing any forwarder methods. The export clause also enables us to rename the original member while exposing it externally.

The export clause follows the same syntax as that of the import statement.

2.1. Export a Member

Let’s examine how we can expose a class member to outside access. We can define a small cache API to describe this scenario:

class CacheImpl {
  private val cache: collection.mutable.Map[String, Int] =
    collection.mutable.Map.empty
  def getFromCache(key: String): Option[Int] = cache.get(key)
  def clear(): Unit = cache.clear()
}
class CacheService {
  private val cacheImpl = new CacheImpl
  export cacheImpl.getFromCache
}

Now, we can use the exported member:

class Usage {
  val service = new CacheService
  service.getFromCache("key")
}

Here, we can access the member getFromCache() of CacheImpl from CacheService without writing a forwarder method in the CacheService. Moreover, the export statement exposes only the getFromCache() method. Therefore, we can’t access clear() from the service as it’s not exported.

We can also export the same members in different classes:

class CacheService { 
  private val cacheImpl = new CacheImpl 
  export cacheImpl.getFromCache 
}
class CacheServiceB { 
  private val cacheImpl = new CacheImpl 
  export cacheImpl.getFromCache
}

The getFromCache() method is accessible from both CacheService and CacheServiceB classes.

2.2. Export a Member as Another Name

We can rename a member while exporting:

export cacheImpl.getFromCache as get

With this export clause, the method getFromCache() is accessible as get(). As a result, here’s how the usage now looks:

class Usage {
  val service = new CacheService
  service.get("key") //as get() instead of getFromCache()
}

This is especially useful for renaming the members without changing the underlying implementation.

2.3. Export given Instances

We can also use the export clause to export all given instances. However, we must explicitly mention the given keyword with the export clause. This avoids accidental export of given instances.

Let’s look at an example with the given instances:

class CacheImpl {
  given timeout: FiniteDuration = 10.millis
  given myStr: String = "Hello"
}
class CacheService {
  private val cacheImpl = new CacheImpl
  export cacheImpl.given
}

Now we can use the CacheService instance to use the exported given instances:

class Usage {
  private val service = new CacheService
  service.timeout //access the given instance for FiniteDuration
  service.myStr //access the given instance for String
}

We can also export given instances selectively. We need to provide the type of the given explicitly to export only for that particular type:

export cacheImpl.given FiniteDuration

This exports the given instance only for the type FiniteDuration without exporting any other types in scope. For example, in the above case, the given instance for String isn’t exported.

2.4. Wildcard Export

We can export all members of a class using a wildcard export. It follows the same style as wildcard import. Let’s look at an example:

class CacheImpl {
  private val cache: collection.mutable.Map[String, Int] =
    collection.mutable.Map.empty
  def getFromCache(key: String): Option[Int] = cache.get(key)
  def clear: Unit = cache.clear()
}
class CacheService {
  private val cacheImpl = new CacheImpl
  export cacheImpl.*
}

As a result, we can use all the members from CacheImpl using CacheService:

class Usage {
  private val service = new CacheService
  service.getFromCache("key")
  service.clear
}

Wildcard export doesn’t export given instances.

Additionally, we can exclude some of the members from wildcard export using the _ symbol along with the export statement:

export cacheImpl.{clear as _, *}

As a result, it exports all members from CacheImpl except the clear() method.

2.5. Export Overloaded Members

The export clause works the same way even if there are overloaded members. It exports all the overloaded methods. Consequently, if we use the clause with rename, it also renames all the overloaded methods.

Let’s see it in action:

class CacheImpl {
  private val cache: collection.mutable.Map[String, Int] =
    collection.mutable.Map.empty
  def getFromCache(key: String): Option[Int] = cache.get(key)
  def getFromCache(key: String, category: String): Option[Int] =
    cache.get(key)
}
class CacheService {
  private val cacheImpl = new CacheImpl
  export cacheImpl.getFromCache as get
}

Now, both the overloaded methods are accessible using the name get() instead of getFromCache():

class Usage {
  val service = new CacheService
  service.get("Key")
  service.get("Key", "category")
}

3. Export Restrictions

There are a few points to consider while using an export statement:

  • It’s not possible to export a member if there’s already another member with the same name.
  • Exported members are always final and can’t be overridden.
  • It’s not possible to export the entire package using wildcard export.

4. Conclusion

In this article, we discussed the newly introduced export clause in Scala 3. We saw different usages of export clauses. We also discussed some points to be noted while using it.

As always, the sample code used in this article is available over on GitHub.