このセクションでは、PythonとScalaのプログラミング言語を比較します。
Pythonに詳しくて、Scalaについて学びたいと考えているプログラマーを対象としています。具体的には、Pythonの言語機能とScalaの比較例を挙げて説明します。
はじめに
例に入る前に、この最初のセクションでは、後に続くセクションの簡単な紹介と概要を提供します。
まず、2つの言語の概要を高レベルで比較し、その後、実践的なプログラミングでの比較を行います。
高レベルでの類似点
高レベルで見ると、ScalaはPythonと以下のような 類似点 を共有しています。
- 高水準プログラミング言語であり、ポインタや手動でのメモリ管理といった低レベルの概念を気にする必要がありません。
- 比較的シンプルで簡潔な構文を持ちます。
- 関数型プログラミングスタイルをサポートしています。
- オブジェクト指向プログラミング(OOP)言語です。
- 内包表記(comprehensions)をサポートしています。Pythonにはリスト内包表記があり、Scalaには
for
内包表記があります。
- ラムダ式と高階関数をサポートしています。
- Apache Sparkを用いたビッグデータ処理に使用できます。
- 優れたライブラリが豊富に使用できます。
高レベルでの相違点
高レベルで見ると、PythonとScalaの間には以下のような 相違点 があります:
- Python は動的型付け言語であり、Scala は静的型付け言語です。
- Pythonは動的型付けですが、型ヒントによる「段階的型付け」をサポートしており、
mypy
のような静的型チェッカーで検証できます。
- Scalaは静的型付けですが、型推論のような機能により動的言語のような感覚で書けます。
- Pythonはインタプリタ型で実行され、Scalaのコードはコンパイルされて .class ファイルになり、Java仮想マシン(JVM)上で動作します。
- JVMでの実行に加えて、Scala.jsによりScalaをJavaScriptの代替として使用することもできます。
- Scala Nativeでは、「システムレベル」のコードを記述し、ネイティブ実行ファイルにコンパイルできます。
- Scalaではすべてが 式 である:
if
文、for
ループ、match
式、さらにはtry
/catch
式でさえも、戻り値を持ちます。
- 慣用的に Scala では不変性を基本とする:不変変数や不変コレクションを使用することが推奨されています。
- Scalaは並行・並列プログラミングのサポートが優れています。
プログラミングレベルでの類似点
このセクションでは、実際に Python と Scala でコードを書く際に見られる類似点を紹介します。
- Scalaの型推論により、動的型付け言語のような感覚でコーディングできます。
- どちらの言語も式の末尾にセミコロンを使用しません。
- 中括弧や括弧ではなく、インデントを重要視した記述がサポートされています。
- メソッド定義の構文が似ています。
- 両方ともリスト、辞書(マップ)、セット、タプルをサポートしています。
- マッピングやフィルタリングに対応した内包表記を備えています。
- 優れたIDEサポートがあります。
- Scala 3のトップレベル定義を利用することで、メソッドやフィールド、その他の定義をどこにでも記述できます。
- 一方で、Pythonはメソッドを1つも宣言しなくても動作できますが、Scala 3ではトップレベルですべてを実現することはできません。たとえば、Scalaアプリケーションを開始するにはmainメソッド(
@main def
)が必要です。
プログラミングレベルでの違い
プログラミングレベルでも、コードを書く際に日常的に見られるいくつかの違いがあります:
- Scalaでのプログラミングは非常に一貫性があります:
- フィールドやパラメータを定義する際に、
val
とvar
が一貫して使用されます
- リスト、マップ、セット、タプルはすべて同様に作成・アクセスされます。たとえば、他のScalaクラスを作成するのと同様に、すべてのタイプを作成するために括弧が使用されます—
List(1,2,3)
, Set(1,2,3)
, Map(1->"one")
- コレクションクラス は一般的にほとんど同じ高階関数を持っています
- パターンマッチングは言語全体で一貫して使用されます
- メソッドに渡される関数を定義するために使用される構文は、匿名関数を定義するために使用される構文と同じです
- Scalaの変数やパラメータは
val
(不変)または var
(可変)キーワードで定義されます
- 慣用的に、Scala では不変データ構造を使うことを良しとします。
- コメント: Pythonはコメントに
#
を使用しますが、ScalaはC、C++、Javaスタイルの //
、/*...*/
、および /**...*/
を使用します
- 命名規則: Pythonの標準は
my_list
のようにアンダースコアを使用しますが、Scalaは myList
を使用します
- Scalaは静的型付けであるため、メソッドパラメータ、メソッドの戻り値、その他の場所で型を宣言します
- パターンマッチングと
match
式はScalaで広範に使用されており、コードの書き方を変えるでしょう
- トレイト(Traits): Scalaではトレイトが多用され、Pythonではインターフェースや抽象クラスがあまり使用されません
- Scalaのコンテキスト抽象と 型推論 は、さまざまな機能のコレクションを提供します:
- 拡張メソッド により、明確な構文を使用してクラスに新しい機能を簡単に追加できます
- 多元的等価性 により、コンパイル時に意味のある比較にのみ等価比較を制限できます
- Scalaには最先端のオープンソース関数型プログラミングライブラリがあります(“Awesome Scala”リストを参照)
- オブジェクト、名前渡しパラメータ、中置表記、オプションの括弧、拡張メソッド、高階関数などの機能により、独自の「制御構造」やDSLを作成できます
- ScalaコードはJVM上で実行でき、Scala NativeやGraalVMを使用してネイティブイメージにコンパイルすることも可能で、高性能を実現します
- その他多くの機能:コンパニオンクラスとオブジェクト、マクロ、数値リテラル、複数のパラメータリスト、交差型、型レベルプログラミングなど
機能の比較と例
この導入に基づき、以下のセクションではPythonとScalaのプログラミング言語機能を並べて比較します。
コメント
Pythonはコメントに # を使用しますが、Scalaのコメント構文はC、C++、Javaなどの言語と同じです。
# a comment
|
// a comment
/* ... */
/** ... */
|
変数の割り当て
これらの例は、PythonとScalaで変数を作成する方法を示しています。
整数変数,文字列変数
x = 1
x = "Hi"
y = """foo
bar
baz"""
|
val x = 1
val x = "Hi"
val y = """foo
bar
baz"""
|
リスト
x = [1,2,3]
|
val x = List(1,2,3)
|
辞書/マップ
x = {
"Toy Story": 8.3,
"Forrest Gump": 8.8,
"Cloud Atlas": 7.4
}
|
val x = Map(
"Toy Story" -> 8.3,
"Forrest Gump" -> 8.8,
"Cloud Atlas" -> 7.4
)
|
集合
x = {1,2,3}
|
val x = Set(1,2,3)
|
タプル
x = (11, "Eleven")
|
val x = (11, "Eleven")
|
Scalaのフィールドが可変になる場合は、変数定義に val
の代わりに var
を使います。
しかし、Scalaの慣習として、特に変数を変更する必要がない限り、常にval
を使います。
関数型プログラミングスタイルのレコード
Scalaのケース・クラスはPythonのフローズン・データクラスに似ています。
構造体の定義
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Person:
name: str
age: int
|
case class Person(name: String, age: Int)
|
インスタンスを作成して使用する
p = Person("Alice", 42)
p.name # Alice
p2 = replace(p, age=43)
|
val p = Person("Alice", 42)
p.name // Alice
val p2 = p.copy(age = 43)
|
オブジェクト指向プログラミングスタイルのクラスとメソッド
このセクションでは、オブジェクト指向プログラミングスタイルのクラスとメソッドに関する機能の比較を行います。
クラスとプライマリーコンストラクタ
class Person(object):
def __init__(self, name):
self.name = name
def speak(self):
print(f'Hello, my name is {self.name}')
|
class Person (var name: String):
def speak() = println(s"Hello, my name is $name")
|
インスタンスを作成して使用する
p = Person("John")
p.name # John
p.name = 'Fred'
p.name # Fred
p.speak()
|
val p = Person("John")
p.name // John
p.name = "Fred"
p.name // Fred
p.speak()
|
1行メソッド
def add(a, b): return a + b
|
def add(a: Int, b: Int): Int = a + b
|
複数行のメソッド
def walkThenRun():
print('walk')
print('run')
|
def walkThenRun() =
println("walk")
println("run")
|
インターフェース、トレイト、継承
Java 8以降に慣れていれば、ScalaのtraitはJavaのインターフェースに良く似ていることに気づくと思います。
Pythonのインターフェース(プロトコル)や抽象クラスがあまり使われないのに対して、Scalaではトレイトが常に使われています。
したがって、この例では両者を比較するのではなく、Scalaのトレイトを使って数学のちょっとした問題を解く方法を紹介します:
trait Adder:
def add(a: Int, b: Int) = a + b
trait Multiplier:
def multiply(a: Int, b: Int) = a * b
// create a class from the traits
class SimpleMath extends Adder, Multiplier
val sm = new SimpleMath
sm.add(1,1) // 2
sm.multiply(2,2) // 4
クラスやオブジェクトでtraitを使う方法は他にもたくさんあります。
しかし、これは概念を論理的な動作のグループに整理して、完全な解答を作成するために必要に応じてそれらを統合するために、どのように使うことができるかのちょっとしたアイデアを与えてくれます。
制御構文
ここではPythonとScalaの制御構文を比較します。
どちらの言語にも if
/else
, while
, for
ループ、 try
といった構文があります。
加えて、Scala には match
式があります。
if
文, 1行
if x == 1: print(x)
|
if x == 1 then println(x)
|
if
文, 複数行
if x == 1:
print("x is 1, as you can see:")
print(x)
|
if x == 1 then
println("x is 1, as you can see:")
println(x)
|
if, else if, else:
if x < 0:
print("negative")
elif x == 0:
print("zero")
else:
print("positive")
|
if x < 0 then
println("negative")
else if x == 0 then
println("zero")
else
println("positive")
|
if
文からの戻り値
min_val = a if a < b else b
|
val minValue = if a < b then a else b
|
メソッドの本体としてのif
def min(a, b):
return a if a < b else b
|
def min(a: Int, b: Int): Int =
if a < b then a else b
|
while
ループ
i = 1
while i < 3:
print(i)
i += 1
|
var i = 1
while i < 3 do
println(i)
i += 1
|
rangeを指定したfor
ループ
for i in range(0,3):
print(i)
|
// preferred
for i <- 0 until 3 do println(i)
// also available
for (i <- 0 until 3) println(i)
// multiline syntax
for
i <- 0 until 3
do
println(i)
|
リスト範囲内のfor
ループ
for i in ints: print(i)
for i in ints:
print(i)
|
for i <- ints do println(i)
|
複数行でのfor
ループ
for i in ints:
x = i * 2
print(f"i = {i}, x = {x}")
|
for
i <- ints
do
val x = i * 2
println(s"i = $i, x = $x")
|
複数の “range” ジェネレータ
for i in range(1,3):
for j in range(4,6):
for k in range(1,10,3):
print(f"i = {i}, j = {j}, k = {k}")
|
for
i <- 1 to 2
j <- 4 to 5
k <- 1 until 10 by 3
do
println(s"i = $i, j = $j, k = $k")
|
ガード付きジェネレータ (if
式)
for i in range(1,11):
if i % 2 == 0:
if i < 5:
print(i)
|
for
i <- 1 to 10
if i % 2 == 0
if i < 5
do
println(i)
|
行ごとに複数のif
条件
for i in range(1,11):
if i % 2 == 0 and i < 5:
print(i)
|
for
i <- 1 to 10
if i % 2 == 0 && i < 5
do
println(i)
|
内包表記
xs = [i * 10 for i in range(1, 4)]
# xs: [10,20,30]
|
val xs = for i <- 1 to 3 yield i * 10
// xs: Vector(10, 20, 30)
|
match
条件式
# From 3.10, Python supports structural pattern matching
# You can also use dictionaries for basic “switch” functionality
match month:
case 1:
monthAsString = "January"
case 2:
monthAsString = "February"
case _:
monthAsString = "Other"
|
val monthAsString = month match
case 1 => "January"
case 2 => "February"
_ => "Other"
|
switch/match
# Only from Python 3.10
match i:
case 1 | 3 | 5 | 7 | 9:
numAsString = "odd"
case 2 | 4 | 6 | 8 | 10:
numAsString = "even"
case _:
numAsString = "too big"
|
val numAsString = i match
case 1 | 3 | 5 | 7 | 9 => "odd"
case 2 | 4 | 6 | 8 | 10 => "even"
case _ => "too big"
|
try, catch, finally
try:
print(a)
except NameError:
print("NameError")
except:
print("Other")
finally:
print("Finally")
|
try
writeTextToFile(text)
catch
case ioe: IOException =>
println(ioe.getMessage)
case fnf: FileNotFoundException =>
println(fnf.getMessage)
finally
println("Finally")
|
マッチ式とパターンマッチは、Scalaプログラミングの大きな要素ですが、ここで紹介しているのは、マッチ式の機能の一部だけです。より多くの例については、制御構造のページをご覧ください。
コレクションクラス
このセクションでは、PythonとScalaで利用可能なコレクションクラスcollections classesを比較します。リスト、辞書/マップ、セット、タプルなどです。
リスト
Pythonにはリストがあるように、Scalaにはニーズに応じて、可変および不可変な列(Seq)のクラスがいくつか用意されています。
Pythonのリストは変更可能であるため、Scalaの ArrayBuffer
によく似ています。
Pythonリスト & Scalaの列(Seq)
a = [1,2,3]
|
// use different sequence classes
// as needed
val a = List(1,2,3)
val a = Vector(1,2,3)
val a = ArrayBuffer(1,2,3)
|
リストの要素へのアクセス
a[0] a[1]
|
a(0) a(1) // just like all other method calls
|
リストの要素の更新
a[0] = 10
a[1] = 20
|
// ArrayBuffer is mutable
a(0) = 10
a(1) = 20
|
2つのリストの結合
リストの反復処理
for i in ints: print(i)
for i in ints:
print(i)
|
// preferred
for i <- ints do println(i)
// also available
for (i <- ints) println(i)
|
Scala の主な列(Seq)クラスは List
、Vector
、ArrayBuffer
です。
List
と Vector
は不変な列が必要なときに使うメインクラスで、 ArrayBuffer
は可変な列が必要なときに使うメインクラスです。
(Scala における 「バッファ」 とは、大きくなったり小さくなったりする配列のことです。)
辞書/マップ
Python の辞書はScala の Map
クラスのようなものです。
しかし、Scala のデフォルトのマップは immutable であり、新しいマップを簡単に作成するための変換メソッドを持っています。
辞書/マップ の作成
my_dict = {
'a': 1,
'b': 2,
'c': 3
}
|
val myMap = Map(
"a" -> 1,
"b" -> 2,
"c" -> 3
)
|
辞書/マップの要素へのアクセス
my_dict['a'] # 1
|
myMap("a") // 1
|
for
ループでの辞書/マップ
for key, value in my_dict.items():
print(key)
print(value)
|
for (key,value) <- myMap do
println(key)
println(value)
|
Scalaには、さまざまなニーズに対応する他の専門的な Map
クラスがあります。
集合
Pythonの集合は、mutable ScalaのSet
クラスに似ています。
集合の作成
set = {"a", "b", "c"}
|
val set = Set(1,2,3)
|
重複する要素
set = {1,2,1}
# set: {1,2}
|
val set = Set(1,2,1)
// set: Set(1,2)
|
Scalaには、他にも様々なニーズに特化したSet
クラスがあります。
タプル
PythonとScalaのタプルも似ています。
タプルの作成
t = (11, 11.0, "Eleven")
|
val t = (11, 11.0, "Eleven")
|
タプルの要素へのアクセス
t[0] # 11
t[1] # 11.0
|
t(0) // 11
t(1) // 11.0
|
コレクションクラスでのメソッド
PythonとScalaには、同じ関数型メソッドがいくつかあります。
Pythonのラムダ式でこれらのメソッドを使うのに慣れていれば、Scalaがコレクション・クラスのメソッドで同じようなアプローチを持っていることがわかると思います。
この機能を実証するために、ここに2つのサンプルリストを示します。
numbers = [1,2,3] // python
val numbers = List(1,2,3) // scala
これらのリストは以下の表で使用され、マップ処理とフィルター処理のアルゴリズムを適用する方法を示しています。
マップ処理の内包表記
x = [i * 10 for i in numbers]
|
val x = for i <- numbers yield i * 10
|
フィルター処理の内包表記
evens = [i for i in numbers if i % 2 == 0]
|
val evens = numbers.filter(_ % 2 == 0)
// or
val evens = for i <- numbers if i % 2 == 0 yield i
|
マップ、フィルター処理の内包表記
x = [i * 10 for i in numbers if i % 2 == 0]
|
val x = numbers.filter(_ % 2 == 0).map(_ * 10)
// or
val x = for i <- numbers if i % 2 == 0 yield i * 10
|
マップ処理
x = map(lambda x: x * 10, numbers)
|
val x = numbers.map(_ * 10)
|
フィルター処理
f = lambda x: x > 1
x = filter(f, numbers)
|
val x = numbers.filter(_ > 1)
|
Scalaのコレクションメソッド
Scalaのコレクションクラスには100以上の関数メソッドがあり、コードを簡単にすることができます。
Python では、これらの関数の一部は itertools
モジュールで利用できます。
map
、filter
、reduce
に加えて、Scala でよく使われるメソッドを以下に示します。
これらのメソッドの例では
c
はコレクションです。
p
は述語です。
f
は関数、無名関数、またはメソッドです。
n
は整数値です。
これらは利用可能なフィルタリング方法の一部です。
|メソッド|説明|
| ————– | ————- |
| c1.diff(c2)
| c1
と c2
の要素の差分を返します。|
| c.distinct
| c
の重複しない要素を返します。|
| c.drop(n)
| 最初の n
要素を除くコレクションのすべての要素を返します。|
| c.filter(p)
| コレクションから、その述語が true
となるすべての要素を返します。|
| c.head
| コレクションの最初の要素を返します。 (コレクションが空の場合は NoSuchElementException
をスローします。)|
| c.tail
| コレクションから最初の要素を除くすべての要素を返します。 (コレクションが空の場合は UnsupportedOperationException
をスローします。)|
| c.take(n)
| コレクション c
の最初の n
個の要素を返します。
以下に、いくつかのトランスフォーマメソッドを示します。|
|メソッド| 説明 |
| ————— | ————-
| c.flatten
| コレクションのコレクション(リストのリストなど)を単一のコレクション(単一のリスト)に変換します。|
| c.flatMap(f)
| コレクション c
のすべての要素に f
を適用し(map
のような動作)、その結果のコレクションの要素を平坦化して、新しいコレクションを返します。|
| c.map(f)
| コレクション c
のすべての要素に f
を適用して、新しいコレクションを作成します。|
| c.reduce(f)
| 「リダクション」関数 f
を c
の連続する要素に適用し、単一の値を生成します。|
| c.sortWith(f)
| 比較関数 f
によってソートされた c
のバージョンを返します。|
よく使われるグループ化メソッド:
| メソッド | 説明 |
| —————- | ————-
| c.groupBy(f)
| コレクションを f
に従って分割し、コレクションの Map
を作成します。|
| c.partition(p)
| 述語 p
に従って2つのコレクションを返します。|
| c.span(p)
| 2つのコレクションからなるコレクションを返します。1つ目は c.takeWhile(p)
によって作成され、2つ目は c.dropWhile(p)
によって作成されます。|
| c.splitAt(n)
| コレクション c
を要素 n
で分割して、2つのコレクションからなるコレクションを返します。|
情報および数学的なメソッド:
| メソッド | 説明 |
| ————– | ————- |
| c1.containsSlice(c2)
| c1
がシーケンス c2
を含む場合に true
を返します。|
| c.count(p)
| p
が true
である c
の要素数を数えます。|
| c.distinct
| c
の一意な要素を返します。|
| c.exists(p)
| コレクション内のいずれかの要素に対して p
が true
であれば true
を返します。|
| c.find(p)
| p
に一致する最初の要素を返します。 要素は Option[A]
として返されます。|
| c.min
| コレクションから最小の要素を返します。 (_java.lang.UnsupportedOperationException_例外が発生する場合があります。)|
| c.max
| コレクションから最大の要素を返します。 (_java.lang.UnsupportedOperationException_例外が発生する場合があります。)|
| c slice(from, to)
| 要素 from
から始まり、要素 to
で終わる要素の範囲を返します。|
| c.sum
| コレクション内のすべての要素の合計を返しますw。 (コレクション内の要素に対して Ordering
を定義する必要があります。)|
以下に、これらのメソッドがリスト上でどのように機能するかを説明する例をいくつか示します。
val a = List(10, 20, 30, 40, 10) // List(10, 20, 30, 40, 10)
a.distinct // List(10, 20, 30, 40)
a.drop(2) // List(30, 40, 10)
a.dropRight(2) // List(10, 20, 30)
a.dropWhile(_ < 25) // List(30, 40, 10)
a.filter(_ < 25) // List(10, 20, 10)
a.filter(_ > 100) // List()
a.find(_ > 20) // Some(30)
a.head // 10
a.headOption // Some(10)
a.init // List(10, 20, 30, 40)
a.intersect(List(19,20,21)) // List(20)
a.last // 10
a.lastOption // Some(10)
a.slice(2,4) // List(30, 40)
a.tail // List(20, 30, 40, 10)
a.take(3) // List(10, 20, 30)
a.takeRight(2) // List(40, 10)
a.takeWhile(_ < 30) // List(10, 20)
これらのメソッドは、Scalaにおける共通のパターンを示しています。オブジェクト上で利用可能な機能メソッドです。
これらの方法はいずれも、初期リスト a
を変更しません。代わりに、コメントの後に示されているデータをすべて返します。
利用可能なメソッドは他にもたくさんありますが、これらの説明と例が、組み込みのコレクションメソッドの持つ力を実感する一助となれば幸いです。
列挙
このセクションでは、PythonとScala 3の列挙型を比較します。
列挙型の作成
from enum import Enum, auto
class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
|
enum Color:
case Red, Green, Blue
|
値とその比較
Color.RED == Color.BLUE # False
|
Color.Red == Color.Blue // false
|
パラメータ化された列挙型
N/A
|
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
|
ユーザー定義による列挙メンバー
N/A
|
enum Planet(
mass: Double,
radius: Double
):
case Mercury extends
Planet(3.303e+23, 2.4397e6)
case Venus extends
Planet(4.869e+24, 6.0518e6)
case Earth extends
Planet(5.976e+24, 6.37814e6)
// more planets ...
// fields and methods
private final val G = 6.67300E-11
def surfaceGravity = G * mass /
(radius * radius)
def surfaceWeight(otherMass: Double)
= otherMass * surfaceGravity
|
Scala 独自の概念
Scalaには、Pythonには現在同等の機能がない概念が他にもあります。
詳細は以下のリンクを参照してください。
- 拡張メソッド(extension methods)、型クラス(type classes)、暗黙的値(implicit values)など、文脈依存の抽象化(contextual abstractions)に関連するほとんどの概念
- Scalaでは複数のパラメータリストを使用できるため、部分適用関数などの機能や独自のDSLを作成することが可能
- 独自の制御構造や DSL を作成できる機能
- [多様等価][多様等価]: どの等価比較が意味を持つかをコンパイル時に制御できる機能
- インフィックスメソッド
- マクロ
Scala と仮想環境
Scalaでは、Pythonの仮想環境に相当するものを明示的に設定する必要はありません。デフォルトでは、Scalaのビルドツールがプロジェクトの依存関係を管理するため、ユーザーは手動でパッケージをインストールする必要がありません。例えば、sbt
ビルドツールを使用する場合、build.sbt
ファイルのlibraryDependencies
設定で依存関係を指定し、
以上のコマンドを実行することで、その特定のプロジェクトに必要なすべての依存関係が自動的に解決されます。
ダウンロードされた依存関係の場所は、主にビルドツールの実装の詳細であり、ユーザーはこれらのダウンロードされた依存関係と直接やりとりする必要はありません。
例えば、sbtの依存関係キャッシュ全体を削除した場合、プロジェクトの次のコンパイル時には、sbtが自動的に必要な依存関係をすべて解決し、ダウンロードし直します。
これはPythonとは異なります。Pythonではデフォルトで依存関係がシステム全体またはユーザー全体のディレクトリにインストールされるため、プロジェクトごとに独立した環境を取得するには、対応する仮想環境を作成する必要があります。
例えば、venv
モジュールを使用して、特定のプロジェクト用に次のように仮想環境を作成できます。
cd myapp
python3 -m venv myapp-env
source myapp-env/bin/activate
pip install -r requirements.txt
これにより、プロジェクトの myapp/myapp-env
ディレクトリにすべての依存関係がインストールされ、シェル環境変数 PATH
が変更されて、依存関係が myapp-env
から参照されるようになります。
Scalaでは、このような手動での作業は一切必要ありません。