Conversions, into & Implicit Resolution

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala 3.10.0 through Scala CLI 1.17.1 on JDK 26 (checked on 2026-10-15). Where a feature depends on a release the page names it and says what the 3.9 LTS and 3.3 LTS lines do; those statements come from compiling the same code with 3.9.0 and 3.3.8 where the text says so, and from the release notes otherwise.

An implicit conversion lets the compiler adapt a value of one type to another type without the caller writing the call. Implicit resolution is the search that finds the conversion, and also every given that a using clause needs. The two share one set of lookup rules, so this page covers them together: first the conversions (the Conversion class, the language import that guards them, and the into mechanism that scopes them to selected parameters), then the rules that decide which given is found and which one wins, and finally how Scala 2 implicits map to Scala 3 and why conversions are used sparingly. The examples use the trailhead scenario. The given and using syntax is introduced on Givens & Using Clauses, extension methods (the preferred alternative to many conversions) on Extension Methods, and the overall design on Contextual Abstractions Overview.

Conversion[A, B]

A conversion is a given instance of scala.Conversion, an abstract class that is also a function:

abstract class Conversion[-T, +U] extends (T => U):
  def apply(x: T): U

The compiler inserts a call to a conversion in these situations:

  • An expression e has type T but the expected type S is something T does not conform to. The compiler looks for a Conversion[T, S] and rewrites e as conversion.apply(e).

  • In a selection e.m, the type of e has no member m. The compiler looks for a conversion from e to a type that has m.

  • In an application e.m(args), the type of e has members named m but none applies to args. The compiler looks for a conversion to a type with a member that does.

The first example defines a conversion in the companion of the target type and one in a separate object, and shows the first two situations:

//> using scala 3.10.0

package trailhead

import scala.language.implicitConversions

case class Trail(name: String, lengthKm: Double)

object Trail:
  given fromName: Conversion[String, Trail] = Trail(_, 1.0)

object Shortcuts:
  given lengthOfName: Conversion[String, Length] = Length(_)
  class Length(val name: String):
    def lengthKm: Double = name.length * 0.5

def describe(t: Trail): String = s"${t.name} (${t.lengthKm} km)"

@main def run(): Unit =
  import Shortcuts.given
  println(describe("Meadow"))              // expected type Trail, found String
  println("Meadow".lengthKm)               // String has no member lengthKm
  println(Trail.fromName("Ridge"))         // calling the conversion explicitly
  val t: Trail = "Pass"
  println(t)
Meadow (1.0 km)
3.0
Trail(Ridge,1.0)
Trail(Pass,1.0)

Three points about where the conversions live:

  • fromName needs no import where a Trail is expected, because the companion object of the target type is part of the implicit scope (see below).

  • lengthOfName converts String to a type with a lengthKm member. A String does not mention Shortcuts anywhere, so the conversion must be imported with import Shortcuts.given. Without it the compiler reports "value lengthKm is not a member of String, but could be made available as an extension method" and suggests the import that would fix it (checked with a version of this program that defined lengthKm on Trail and forgot the import).

  • A conversion can be called by name, as Trail.fromName("Ridge") shows, and because Conversion extends the function type T => U it can also be passed where a function is expected.

An import of Shortcuts.* would not bring lengthOfName: wildcard imports leave out givens, and given selectors are required (see Givens & Using Clauses).

The implicitConversions import and feature warnings

Conversions are the most abused part of Scala 2, so Scala 3 asks you to say that you want them. Using a given conversion without the language feature enabled compiles but reports a feature warning at the place where the conversion is inserted (the warning is shown with -feature; without it the compiler prints only "there was 1 feature warning"):

//> using scala 3.10.0
//> using option -feature

case class Trail(name: String, lengthKm: Double)

given Conversion[String, Trail] = Trail(_, 1.0)

@main def run(): Unit =
  val t: Trail = "Pass"
  println(t)
[warn] ./a2f.scala:9:18
[warn] Use of implicit conversion given instance given_Conversion_String_Trail should be enabled
[warn] by adding the import clause 'import scala.language.implicitConversions'
[warn] or by setting the compiler option -language:implicitConversions.
[warn] See the Scala docs for value scala.language.implicitConversions for a discussion
[warn] why the feature should be explicitly enabled.
[warn]   val t: Trail = "Pass"
[warn]                  ^^^^^^
Trail(Pass,1.0)

The rules for the warning are:

  • A Scala 3 conversion, a given of type Conversion, warns where the compiler inserts it. Defining it does not warn.

  • A Scala 2 style conversion, an implicit def with one parameter, warns where it is defined, and nothing warns at the call sites. The same file with implicit def nameToTrail(name: String): Trail reports "Definition of implicit conversion method nameToTrail should be enabled" at the definition.

  • The warning is silenced by import scala.language.implicitConversions in the scope of the definition or of the use, or by the compiler option -language:implicitConversions for the whole project.

  • The reference says the feature warning is planned to become an error in a future Scala 3 version. Treat the import as mandatory.

The import is a deliberate opt-in per file. It also lets the compiler propagate expected types more widely in code that did not opt in, which improves type inference. The next section shows how to allow conversions in one place only.

into: scoped conversions (SIP-71)

A library often wants conversions for a few parameters, for example to accept an Int where it needs a Distance, without forcing every user to import implicitConversions. The into mechanism (SIP-71) does that. It was a preview feature in 3.8, is stable since Scala 3.9.0 and is therefore stable in 3.10; on the 3.3 LTS line it does not exist (the same code does not compile with 3.3.8, which reports "value Conversion is not a member of scala" for import scala.Conversion.into).

There are two forms, into as a type constructor and into as a modifier.

into as a type constructor

into[T] is an opaque type, defined in the companion of Conversion, that marks a type as a conversion target. Where the expected type is into[T], a conversion to T is inserted with no import and no warning:

//> using scala 3.10.0
//> using option -feature

package trailhead

import scala.Conversion.into

case class Distance(km: Double)

given Conversion[Int, Distance] = Distance(_)

def total(stages: into[Distance]*): Distance = Distance(stages.map(_.km).sum)

def plain(d: Distance): Distance = d

@main def run(): Unit =
  println(total(3, Distance(2.5), 4))   // each argument converted on its own
  println(plain(5))                     // outside `into`: feature warning
[warn] ./i1.scala:18:17
[warn] Use of implicit conversion given instance given_Conversion_Int_Distance in package trailhead should be enabled
[warn] by adding the import clause 'import scala.language.implicitConversions'
[warn] or by setting the compiler option -language:implicitConversions.
[warn] See the Scala docs for value scala.language.implicitConversions for a discussion
[warn] why the feature should be explicitly enabled.
[warn]   println(plain(5))                     // outside `into`: feature warning
[warn]                 ^
Distance(9.5)
Distance(5.0)

The call to total converts only the Int arguments, one by one, because into applied to a vararg parameter converts each argument on its own. The call to plain is outside any into and gets the feature warning. The same program compiles and runs the same on 3.9.0.

into also works in other positions that the compiler treats as expected types: parameters of enum cases (so that a tree type accepts Add(1, Neg(2)) where 1 is an Int converted to a Const), and the result of a function argument. The second example shows both, plus what happens inside the method body: the compiler erases into from the types of parameters as seen in the body, so a.km needs no unwrapping. Outside a body, .underlying of Conversion takes the wrapper off:

//> using scala 3.10.0
//> using option -feature

package trailhead

import scala.Conversion.into

case class Distance(km: Double)
given Conversion[Int, Distance] = Distance(_)

def longest(stages: List[Int], f: Int => into[Distance]): Distance =
  stages.map(f).map(_.km).maxOption.fold(Distance(0))(Distance(_))

def sum(a: into[Distance], b: into[Distance]): Double =
  a.km + b.km        // inside the body the parameters are plain Distance

@main def run(): Unit =
  println(longest(List(1, 2, 3), n => n * 2))   // lambda result converted
  println(sum(3, Distance(1.5)))
  val d: into[Distance] = Distance(2)
  import Conversion.underlying
  val plain: Distance = d.underlying
  println(plain)
Distance(6.0)
4.5
Distance(2.0)

An enum shows the intended use for a tree or a small DSL: only the constructor parameters convert.

//> using scala 3.10.0
//> using option -feature

import scala.Conversion.into

enum Expr:
  case Neg(e: into[Expr])
  case Add(e1: into[Expr], e2: into[Expr])
  case Const(n: Int)
import Expr.*

given Conversion[Int, Const] = Const(_)

def eval(e: Expr): Int = e match
  case Const(n)  => n
  case Neg(e)    => -eval(e)
  case Add(a, b) => eval(a) + eval(b)

@main def run(): Unit =
  println(eval(Add(1, Neg(2))))
-1

A type parameter instantiated to an into type also counts (List[into[Keyword]]("if", "then", "else") converts the strings), but the conversion target is only recognised where the type is fully known. The reference warns that a value declared as List[into[Keyword]] and built from List(ifKW, "then", "else") does not get the permission, and in 3.10 that is reported as the ordinary feature warning for the two strings, not as an error (checked with -feature).

into as a modifier

If you own the target type, mark it instead of changing every signature. into is a soft modifier on classes, traits and opaque type aliases, and conversions into the type then need no import wherever it is the expected type:

//> using scala 3.10.0
//> using option -feature

package trailhead

import scala.Conversion.into

into trait Waypoint
case class Summit(name: String) extends Waypoint
case class Hut(name: String) extends Waypoint

given Conversion[String, Summit] = Summit(_)
given Conversion[String, Waypoint] = Summit(_)

def visit(w: Waypoint): String = s"visit $w"

@main def run(): Unit =
  println(visit("Matterhorn"))
visit Summit(Matterhorn)

This form is meant for a small set of designated conversion targets, such as a framework type used as a parameter type everywhere. The reference states the limit plainly: keep the number of into types to the absolute minimum, and never make a type into just in case someone might want to add a conversion later.

A valid conversion target type, the one that allows conversions without the import, is one of these: into[T]; a class, trait or opaque type declared into, with any type arguments; a type alias, match type, annotated type or refined type whose underlying type is valid; a union or intersection of two valid types; or a type parameter explicitly instantiated to a valid type. The reference motivates the feature with the list concatenation xs ys` where `ys` is an array: the parameter of ` would be declared as into[IterableOnce[A]] so that the array converts without an import at the call site.

Where the compiler looks for a given

Conversions and using arguments are found by the same search. Two scopes are consulted, in this order:

flowchart TD Q["Need a given of type T"] --> L["1. Lexical scope: givens defined, inherited or imported at the call site, nearest enclosing scope first"] L -->|"found"| W["Pick the nearest; several at the same level: use the priority and specificity rules"] L -->|"none found"| I["2. Implicit scope of T: companions of T, its parents and its type arguments, plus the objects on the path of each anchor"] I -->|"found"| W I -->|"none found"| E["Error: no given instance of type T was found"]

The lexical scope wins. The implicit scope is looked at only when the lexical scope has no candidate. The example uses a companion given and an imported one, and a wildcard import that does not bring givens:

//> using scala 3.10.0

package trailhead

class Units(val name: String)
object Units:
  given fromCompanion: Units = Units("companion")

object Presets:
  given metric: Units = Units("imported")

def name(using u: Units): String = u.name

@main def run(): Unit =
  println(name)                          // only the implicit scope applies
  locally:
    import Presets.given
    println(name)                        // the lexical scope wins
  locally:
    import Presets.*
    println(name)                        // wildcard does not import givens
companion
imported
companion

Adding a local given local: Units next to import Presets.given in the same block makes the call ambiguous: "Ambiguous given instances: both given instance metric in object Presets and given instance local match type trailhead.Units". Two givens at the same nesting level are compared by the specificity and priority rules below, and when neither is better the search fails.

The implicit scope of a type is built from its anchors: the companion objects of the class and of its parents, the companions of its type arguments, and the objects on the path to the type (for p.o.Trail, the object o). A given in the companion of Trail is therefore found anywhere a Trail is needed, without any import. This is why the companion is the default home for the instances of a type class: see Type Classes.

Changes in implicit resolution

Scala 3 changed several rules of the Scala 2 search. They apply to given and to old-style implicit alike.

Nesting

A definition nested more deeply wins over an outer one, so the nearest given is picked instead of reporting an ambiguity. Scala 2 reports the ambiguity. The same program, one version in each language:

//> using scala 3.10.0

package trailhead

class Units(val name: String)

def outer(using a: Units): String =
  def inner(using b: Units): String = summon[Units].name   // b is nested deeper than a
  inner(using Units("inner-parameter")) + " / " + summon[Units].name

def shadow(using a: Units): String =
  given local: Units = Units("local-given")
  summon[Units].name

@main def run(): Unit =
  given global: Units = Units("global")
  println(outer)
  println(shadow)
inner-parameter / global
local-given

The same shape in Scala 2.13.18, with implicit parameters, fails:

[error] ./r1s2.scala:7:44
[error] ambiguous implicit values:
[error]  both value a of type Units
[error]  and value b of type Units
[error]  match expected type Units
[error]     def inner(implicit b: Units): String = implicitly[Units].name
[error]                                            ^^^^^^^^^^^^^^^^^^

Package prefixes

A package prefix no longer contributes to the implicit scope of a type. A given defined at package level next to a type is not in the implicit scope of that type from other packages, whereas a given in an object that the type lives in is. The two files below define a type in an object o inside package p, with one given in o and one at the package level, and use it from package q:

//> using scala 3.10.0

package p

trait Show[T]:
  def show(t: T): String

object o:
  class Trail(val name: String)
  given inCompanionPath: Show[Trail] = t => s"o: ${t.name}"

given inPackage: Show[o.Trail] = t => s"p: ${t.name}"
package q

import p.{Show, o}

@main def run(): Unit =
  val t = o.Trail("Ridge")
  println(summon[Show[o.Trail]].show(t))
o: Ridge

Only the given in o is found. Put instances in the companion or in an object on the path of the type, or import them.

Ambiguity propagates

If an ambiguity is found while the compiler is resolving the arguments of a given, it is reported to the caller instead of silently discarding that candidate (Scala 2 treated it as a plain failure of the candidate and went on, which people used to simulate negation; use scala.util.NotGiven for that now). Here b needs an A, and there are two:

//> using scala 3.10.0

package trailhead

class A
class B
trait Show[T]:
  def show: String
given a1: A = A()
given a2: A = A()
given b(using a: A): Show[B] = new Show[B] { def show = "b" }

@main def run(): Unit = println(summon[Show[B]].show)
[error] ./s2q.scala:13:48
[error] No best given instance of type trailhead.Show[trailhead.B] was found for parameter x of method summon in object Predef.
[error] I found:
[error]
[error]     trailhead.b(
[error]       /* ambiguous: both given instance a1 in package trailhead and given instance a2 in package trailhead match type trailhead.A */
[error]         summon[trailhead.A]
[error]     )
[error]
[error] But both given instance a1 in package trailhead and given instance a2 in package trailhead match type trailhead.A.
[error]   println(summon[Show[B]].show)
[error]                                                ^

Adding a plain given Show[B] next to it makes the program compile and print plain: it is then the only candidate that does not depend on the ambiguous A. Divergence is also treated differently: a divergent candidate is a normal failure after which other candidates are still tried.

Given prioritization (3.7)

When several givens match, Scala 3.0 to 3.6 picked the one with the most specific type. Scala 3.7 picks the most general one that satisfies the request. The reference sums it up with this example, in which the old rules reported an ambiguity:

//> using scala 3.10.0

package trailhead

class A
class B extends A
class C extends A

given A = A()
given B = B()
given C = C()

@main def run(): Unit =
  println(summon[A].getClass.getSimpleName)
  println(summon[B].getClass.getSimpleName)
A
B

The same file gives different answers by release (checked): 3.9.0 and 3.10.0 print A and B. With Scala 3.3.8 and with -source:3.5 the first summon is rejected with "Ambiguous given instances: both given instance given_B …​ and given instance given_C …​ match type trailhead.A". Compiling with -source:3.7-migration keeps the new behaviour and prints a warning at each place where the choice changed ("Given search preference for trailhead.A between alternatives …​ has changed. Previous choice: the first alternative. Choice from Scala 3.7: the second alternative.").

The consequence for library design: a given for a general type now shadows more specific instances that sit at the same level, so a base-type fallback no longer makes the specific instances ambiguous. Other rules that changed in 3.0: a given that takes using parameters is less specific than one that takes none, all else being equal; the old Scala 2 preference for conversions with call-by-value over call-by-name parameters is gone; and the type of an implicit val or implicit def has to be written explicitly (except in local blocks).

Inaccessible companion givens (deprecated in 3.9, not found in 3.10)

Implicit search used to look into the companion of a type even when the call site could not refer to that companion, so a private companion could still contribute givens. The specification has always required an eligible given to be accessible at the point of the call. Scala 3.9 reports the use as a deprecation warning, and Scala 3.10 stops finding such instances:

//> using scala 3.10.0

package trailhead

object api:
  class Config(val label: String)
  private object Config:
    given Config = Config("hidden default")
  def describe(using c: Config): String = c.label

@main def run(): Unit = println(api.describe)
[error] ./h1.scala:11:45
[error] No given instance of type trailhead.api.Config was found for parameter c of method describe in object api
[error]
[error] The following import might fix the problem:
[error]
[error]   import trailhead.api.Config.given_Config
[error]
[error] @main def run(): Unit = println(api.describe)
[error]                                             ^

With 3.9.0 and -deprecation the same file compiles and prints hidden default, and the compiler says: "Usage of implicit given instance given_Config defined in object Config, which is not accessible here. In Scala 3.10, this implicit will no longer be found." Without -deprecation 3.9.0 reports only "there was 1 deprecation warning". With 3.3.8 the file compiles and prints hidden default. The fix, for a library, is to make the companion public or to move the given to a public object. The release notes say that about 20 projects in the community build stopped compiling on 3.10 for this reason, and that a user often cannot fix it in their own code because the instance lives in a library.

Passing explicit arguments to implicit parameters (3.7 warning)

Scala 3 writes explicit arguments for a context parameter with using, so that it is never confused with an argument to the apply of the result. For a Scala 2 style implicit parameter, omitting using is allowed but, since 3.7, warns:

//> using scala 3.10.0

package trailhead

case class Config(label: String)

def withImplicit(implicit c: Config): String = c.label
def withUsing(using c: Config): String = c.label

@main def run(): Unit =
  println(withImplicit(Config("explicit")))
  println(withImplicit(using Config("explicit using")))
  println(withUsing(using Config("given")))
[warn] ./x1.scala:11:24
[warn] Implicit parameters should be provided with a `using` clause.
[warn] This code can be rewritten automatically under -rewrite -source 3.7-migration.
[warn] To disable the warning, please use the following option:
[warn]   "-Wconf:msg=Implicit parameters should be provided with a `using` clause:s"
[warn]   println(withImplicit(Config("explicit")))
[warn]                        ^^^^^^^^^^^^^^^^^^
explicit
explicit using
given

For a real using clause the plain form is an error: withUsing(Config("explicit")) does not mean "pass this argument" but "no given for parameter c", and the compiler reports "No given instance of type Config was found for parameter c of method withUsing". Run with 3.3.8 the first line compiles without a warning. The rewrite -rewrite -source 3.7-migration inserts the using keyword for you.

Simulating Scala 2 implicits in Scala 3

Scala 3 still compiles the old implicit keyword (with the warnings above), and it keeps both forms in the common subset of 2.13 and 3. The reference maps each Scala 2 construct to a Scala 3 one:

Scala 2 Scala 3 Notes

implicit val x: T = ...

given x: T = ..., or a regular val plus given T = x

given is lazy unless the right-hand side is a simple reference; keep an eager val and an alias given when evaluation order matters.

implicit def f[T](implicit o: Ord[T]): Ord[List[T]]

given [T](using o: Ord[T]): Ord[List[T]] with ... (see Givens & Using Clauses)

A parameterised given maps to a class plus an implicit method in Scala 2 terms. Both the with and the [T] => (o: Ord[T]) => Ord[List[T]] forms compiled without warnings on 3.10.0.

def m(implicit c: C)

def m(using c: C)

Explicit arguments are passed with (using ...). implicitly[C] becomes summon[C], which may return a more precise type.

implicit def conv(x: A): B

given Conversion[A, B] = ...

The language import is still needed (see above).

implicit class RichT(t: T)

extension (t: T) def ... (see Extension Methods)

Other uses of implicit classes become a regular class plus a Conversion.

implicit object

given name: T with ... or given name: T:

Unparameterised givens map to implicit objects.

The program below mixes the Scala 2 forms (in Scala2Style) and their Scala 3 replacements (in Scala3Style), and compiles with -feature -deprecation:

//> using scala 3.10.0
//> using option -feature
//> using option -deprecation

package trailhead

case class Trail(name: String, lengthKm: Double)
case class Config(label: String)

object Scala2Style:
  implicit val defaultConfig: Config = Config("scala2")
  implicit def nameToTrail(name: String): Trail = Trail(name, 1.0)
  implicit class RichTrail(t: Trail):
    def isLong: Boolean = t.lengthKm > 10
  def label(implicit c: Config): String = implicitly[Config].label

object Scala3Style:
  given Config = Config("scala3")
  given Conversion[String, Trail] = Trail(_, 1.0)
  extension (t: Trail) def isLong: Boolean = t.lengthKm > 10
  def label(using Config): String = summon[Config].label

@main def run(): Unit =
  import Scala2Style.*
  println(label)
  println(Trail("a", 12).isLong)
  val t: Trail = "x"
  println(t)
  println(Scala3Style.label(using Config("explicit")))
[warn] ./s1.scala:12:16
[warn] Definition of implicit conversion method nameToTrail should be enabled
[warn] by adding the import clause 'import scala.language.implicitConversions'
[warn] or by setting the compiler option -language:implicitConversions.
[warn] See the Scala docs for value scala.language.implicitConversions for a discussion
[warn] why the feature should be explicitly enabled.
[warn]   implicit def nameToTrail(name: String): Trail = Trail(name, 1.0)
[warn]                ^
scala2
true
Trail(x,1.0)
explicit

The reference expected old-style implicits to be deprecated in a later version; on 3.10.0 they still compile (with the warnings shown), and the compiler option -source:future previews their removal. Compiling the same file with -source:future rejects each Scala 2 form: “implicit` parameters are no longer supported, use a using clause instead", "`implicit` vals are no longer supported, use a given clause instead", "`implicit` conversion methods are no longer supported. They can usually be replaced by given instances of class Conversion” and "`implicit classes are no longer supported. They can usually be replaced by extension methods". The -rewrite -source future-migration options turn each into the Scala 3 form. For projects that still cross-compile with Scala 2.13 see Scala 2 & Scala 3 Compatibility and Migrating from Scala 2.

The Scala 3.10 compiler also removes some conversions that user code relied on without knowing: runtime.RichInt and the other Rich* implicit classes are gone, and Predef.ArrowAssoc is deprecated and no longer an implicit conversion. A library that defined -> through its own implicit class can see Predef.-> chosen instead (release notes).

Why conversions are discouraged

A conversion changes the meaning of the code at the place it is inserted, and the reader cannot see it there. The classic example is a lossy conversion:

//> using scala 3.10.0
//> using option -feature
import scala.language.implicitConversions

given Conversion[Double, Int] = _.toInt

def pages(n: Int): String = s"$n pages"

@main def run(): Unit =
  println(pages(3.99))
  val d: Double = 3.99
  val n: Int = d
  println(n)
3 pages
3

Scala would never narrow a Double to an Int silently, and one given quietly changes that for the whole file. The problems are the same as in Scala 2:

  • The inserted call is invisible at the use site, so reading pages(3.99) tells you nothing about the truncation.

  • They can add up: two conversions in scope can make an expression typecheck in a way no one intended, or change which overload is picked.

  • They hide cost and failure: a conversion can allocate, parse or throw.

  • They weaken error messages and, by letting the compiler widen what it accepts, type inference.

  • Library authors cannot control where users enable them. That is the reason for into.

The alternatives, in order of preference:

  • Extension methods for "this type should have more methods". The Scala 3 Book recommends them in place of Scala 2’s implicit classes: see Extension Methods.

  • Type classes for "this operation works for many types": see Type Classes.

  • Overloading or default arguments for a few argument shapes.

  • Explicit named conversions, toX or fromX methods or apply in the companion.

  • into when conversion on the argument really is the API, as in the xs ++ ys example of the reference.

The one pattern for which conversions are still the idiomatic tool is the magnet pattern: a method takes a "magnet" type into which several argument types convert, instead of overloads that would explode combinatorially. With into trait or into[...] it needs no import for the callers; this version uses the language import for clarity:

//> using scala 3.10.0
import scala.language.implicitConversions

enum Arg:
  case Name(text: String)
  case Count(n: Int)
  case Flag(on: Boolean)

object Arg:
  given Conversion[String, Arg] = Name(_)
  given Conversion[Int, Arg] = Count(_)
  given Conversion[Boolean, Arg] = Flag(_)

def render(args: Arg*): String = args.map {
  case Arg.Name(t)  => s"name:$t"
  case Arg.Count(n) => s"count:$n"
  case Arg.Flag(on) => s"flag:$on"
}.mkString(", ")

@main def run(): Unit = println(render("ridge", 3, true))
name:ridge, count:3, flag:true

Conversions can also be used inside an instance, as a way to derive one type class instance from another through a view:

//> using scala 3.10.0
import scala.language.implicitConversions

class A(val x: Int)
given Conversion[A, Int] = _.x
given [T, S](using conv: Conversion[T, S], ordS: Ordering[S]): Ordering[T] = (x, y) => ordS.compare(conv(x), conv(y))

@main def run(): Unit = println(List(A(3), A(1), A(2)).sorted.map(_.x))
List(1, 2, 3)

Java and Kotlin

  • Java has a fixed set of implicit conversions: primitive widening (int to long), boxing and unboxing, and a few reference conversions. A program cannot add its own. Scala has the same for its numeric types and boxing (the standard library defines Int to java.lang.Integer as a conversion), and lets you define more.

  • Kotlin has no implicit conversions; numeric widening must be explicit (toLong()), and the use cases of implicit classes are covered by extension functions. See Kotlin Extension Functions & Scope Functions.

Where the books are outdated

  • Books and posts on Scala 2 present the implicit scope as including the package prefix, and describe an ambiguity between a nested and an outer implicit. Both differ in Scala 3.

  • Descriptions of "given prioritization" that say the most specific given wins describe Scala 3.0 to 3.6.

  • Books describe implicit class as the way to add methods; use extension.

  • Material before 3.8 says conversions always need import scala.language.implicitConversions. Since into (stable in 3.9) the import can be avoided for selected targets.

  • Private companion givens were found until 3.9. Code and answers that rely on it do not compile on 3.10.

Scala 3.9 LTS versus 3.10

  • Conversion and the feature warning are the same on 3.3, 3.9 and 3.10.

  • into is stable in 3.9 and 3.10 (checked with 3.9.0), preview in 3.8 and absent in 3.3 (checked with 3.3.8: no Conversion.into, no into modifier).

  • The given prioritization of 3.7 applies on 3.9 and 3.10 (checked with 3.9.0) and not on 3.3 (3.3.8 reports the ambiguity).

  • Inaccessible companion givens are found with a deprecation warning on 3.9 (visible with -deprecation) and not found on 3.10. Compile library code with 3.9 and -deprecation before moving to 3.10.

  • The 3.7 warning for explicit arguments to implicit parameters is on 3.9 and 3.10, not on 3.3.