パート1.機能的
この記事(完全に正直に言うと一連のメモ)は、初心者だけでなく、後輩だけでなく、灰色のひげを持つ経験豊富なプログラマーにもScalaの道を歩むときに犯す間違いについて説明しています。 それらの多くは、C、C ++、Javaなどの命令型言語でのみ常に機能しているため、Scalaのイディオムは理解できず、さらに明らかではありません。 したがって、私は自由に、改宗者に警告し、彼らの典型的な過ちについて話します-完全に無実であり、死によって罰せられるScalaの世界の過ちです。
パブリケーション構造:
参加する代わりに
私のキャリアの最初に、私は非常に興味深い状況にいることに気づきました。私は当時まだ若い開発者でしたが、私の年上の同僚に岩のようなイディオムを説明しなければなりませんでした。 それはとても起こりました-そして、私はこの大いに貴重な経験に人生に感謝しています。 現在、私が働いている会社には社内の従業員トレーニングシステムがあるため、Scalaの開発を支援するために、小規模から大規模まで、あらゆるレベルの開発者を支援しています。 現時点では、私は他のメンターと一緒にScalaコースのテストとサポートを行っています。
もともと、この記事は英語で、「ジュニアとジュニアシニア向けのスカラ」というタイトルで書かれていました。 しかし、ロシア語のテキストで作業する方がはるかに高速で便利であることが判明したため、タイトルに翻訳できないしゃれを犠牲にしなければなりませんでした。 この記事は、純粋な後輩だけでなく、命令型プログラミングの経験に関係なく、Scala言語に精通しているすべての人を対象としています。
この記事は、概して、実用的なヒントの寄せ集めであり、その観点から、資料のプレゼンテーションの複雑なマルチレベルの学術的構造を欠いています。 代わりに、この記事は2つのパートに分かれています。最初はScalaでの関数型プログラミングのイディオムについて説明し、2番目はオブジェクト指向のイディオムについて説明します。 そして、Scalaの最も過小評価されている機能-タイプエイリアスから始めます。
タイプエイリアスについて
これまでtypedef
の経験がなかった多くの初心者開発者にとって、この言語の機能は役に立たないように思われます。 ただし、これは完全に真実ではありません。Cでは、型エイリアスはすべてのステップで標準ライブラリによって使用され、プラットフォーム間のコードの移植性を確保する手段の1つです。 さらに、それらはコードの読みやすさを大幅に改善します。コードには、間接性の大きなポインターが含まれます。 よく知られた例- signal
関数の標準的な宣言は完全に読めません:
void (*signal(int sig, void (*func)(int)))(int);
ただし、シグナルハンドラー関数へのポインターのエイリアスを追加すると、この問題は解決します。
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
C ++では、型エイリアスなしでは静的メタプログラミングは考えられません。さらに、テンプレート型の完全な名前に夢中にならないようにします。
OcamlやHaskellなどの言語にはtype
キーワードがあり、そのセマンティクスはScalaの場合よりもはるかに複雑です。 たとえば、 type
キーワードを使用すると、 型の同義語だけでなく、 代数的なデータ型も作成できます。
(* Ocaml*) type suit = Club | Diamond | Heart | Spade;;
これは、OcamlとSMLで作成することもできます。
ユニオン型(ユニオン型):
(* OCaml *) type intorstring = Int of int | String of string;; (* SML *) datatype intorreal = INT of int | REAL of real
現時点(Scala 2.12)では、方法がわかりません。typeキーワードを使用して宣言された機能は、 type
同義語化に限定され、パス依存型を宣言します。 ただし、将来のバージョンでは、この機能が追加される予定です( 説明については seniaに感謝します )。 既に既知のタイプに他の名前を付ける必要があるのはなぜですか? まず、これは追加のセマンティックロードを追加します。たとえば、 DateString
型の意味は単なるString
よりも理解しやすくなり、 Map[Username, Key]
はMap[String, String]
よりも良く見えます。 第二に、同義語化により、大きく複雑なタイプの署名を減らすことができます。 Map[Username, Key]
は見栄えが良いですが、 Keystore
ずっと短く、より理解しやすいです。
もちろん、シノニム型には欠点もありますPerson
型が表示され、オブジェクト、クラス、またはエイリアスであるかどうかを理解できません。
このツールを悪用することは間違いなく価値がありませんが、実際に役立つ状況がいくつかあります。
- 特定のアクションに関連付けることができる関数があります。名前を変更しても、
() => Unit
などの署名で読者を怖がらせることはできません。このタイプの関数をAction
という名前に関連付けます。 -
[, []]
抽象化してい[, []]
- 既存のタイプに追加のセマンティック情報を追加します
ここでより多くの例を見つけることができます。例のセクションまで少し下にスクロールしてください。
割り当てについてもう一度
Scalaの割り当ては、あなたが慣れているものではありません。 この操作を見てみましょう。一見したほど簡単ではないことがわかります。
// , scala> val address = ("localhost", 80) address: (String, Int) = (localhost,80) scala> val (host, port) = address host: String = localhost port: Int = 80
タプルを2つの変数にソートしたばかりですが、問題はタプルに限定されません。
scala> val first::rest = List(1,2,3,4,5) first: Int = 1 rest: List[Int] = List(2, 3, 4, 5)
case class
同様の操作を実行できます。
case class Person(name: String, age: Int) val max = Person("Max", 36) // max: Person = Person(Max,36) val Person(n, a) = max // n: String = Max // a: Int = 36
さらに:
scala> val p @ Person(n, a) = max // p: Person = Person(Max,36) // n: String = Max // a: Int = 36
後者の場合、名前p
でcase class
レコード自体を取得し、名前n
で名前を取得します(a-年齢)。
洗練された読者は、割り当てがパターンマッチングとまったく同じように動作することにすでに気付いています。 同様の機能は、PythonやErlangなどの他の言語でも実装されています。 この機能は、主にデータ構造を解凍するために使用します。 ただし、乱用しないでください。複雑なデータ構造を解凍すると、読みやすさが大幅に低下します。
オプション
多くの人はすでにJava 8のOptional
型に精通しています。Scalaでは、 Option
型は同じ機能を実行します。 また、多くのJava支持者にとって、このタイプはGuavaのGoogleライブラリから知られているかもしれません。
はい、 Optional
使用してnull
を回避し、後でNullPointerException
を回避しnull
。 はい、 isEmpty
およびnonEmpty
ます。 GuavaバリアントにはisPresent
メソッドがあります。 また、Javaやその他の言語でOptional
を使用したか使用していない多くの人がScalaでOptional
を誤用しています。
ただし、Optional
、Skalovmap
と同様に動作する同じGuavaで定義されたtransform
メソッドOptional
あることに気づいているわけではありません。
Option
誤用は一般的な問題です。 Option
、 まず、存在しない可能性のあるエンティティを概念的に示すために必要であり、NPEから逃げないために必要です。 はい、問題があり、問題は深刻です。 誰かがこれのために彼自身の言語を発明しさえします。 しかし、ScalaのOption
の誤用に戻りましょう。
if (option.isEmpty) default else // c NoSuchElementException ( ) option.get
チェックを行っていますが、ここで爆発するものはないようです。 私を信じてください、あなたは産業コードで間違いを犯すことができます、そして、条件は予想された表現でないかもしれません。 また、テストのスペルも間違っている可能性があります。 あなたによってではなく、あなたの前任者によって。
一般に、上記の例には別の問題があります。 あなたのフローは何らかのブール値に依存しており、その整合性が侵害されています。
一部の開発者は、すでに「機能する」コード用にテストをカスタマイズする機能を備えています。 上記のコードよりも正確で短いコードは、次のように記述できます。
option getOrElse default
コードをコンパクトにすればするほど、コード内のエラーを見つけやすくなり、ミスを犯しにくくなります。 さまざまなOption
を連鎖できる便利なorElse
メソッドがあります。
Option内に値が存在する場合、その値を変換する必要があります。 これにはmap
メソッドがあります。値を取得して変換し、コンテナに戻します。
val messageOpt = Some("Hello") val updMessageOpt = messageOpt.map(msg => s"$mgs cruel world!") updMessageOpt: Option[String]
そして時々それは次のように起こります:
val messageOptOpt = Some(Some("Hello"))
Option
は非常にネストできます。 この問題は、 flatMap
メソッドまたはflatten
メソッドによって解決されます。 最初はmap
と同様に機能しmap
-内部値を変換しますが、同時に構造を単純化し、2番目は単純に構造を単純化します。
Option
を返す特定の関数があると想像してください:
def concatOpt (s0: String, s1: String) = Option(s0 + s1)
その後、同様の結果が得られます。
messageOpt.map(msg => concatOpt(msg, " cruel world")) res0: Option[Option[String]] = Some(Some(Hello cruel world)) // `flatMap`: messageOpt.flatMap(msg => concatOpt(msg, " cruel world")) res6: Option[String] = Some(Hello cruel world) // flatten messageOptOpt.flatten == Some("Hello") res1: Option[String] = Some(Hello)
Scalaには、 Option
た作業を大幅に促進できる別のメカニズムがあり、「理解のため」という名前で知られている場合があります。
val ox = Some(1) val oy = Some(2) val oz = Some(3) for { x <- ox; y <- oy; z <- oz } yield x + y + z // res0: Option[Int] = 6
Option
タイプのいずれかがNone
に等しい場合、 yield
後yield
ユーザーは空のコンテナー、または空のコンテナーの値を受け取ります。 Option
の場合Option
これはNone
です。 リストの場合、 Nil
。
そして最も重要なことは、 get
メソッドを呼び出さないようにするためにあらゆることを試みることです。 これは潜在的な問題につながります。
私はあなたがよくやったとすべてをチェックしたことを知っています。 あなたのお母さんもそう思うと思いますが、これはあなたget
再び引っ張る理由を与えません。
リスト
Option
はget
あり、リストにはhead
、 init
およびtail
もありinit
。 空のリストで上記のメソッドを呼び出すことで取得できるものを次に示します。
// : init: java.lang.UnsupportedOperationException head: java.lang.NoSuchElementException last: java.lang.NoSuchElementException tail: java.lang.UnsupportedOperationException
もちろん、空のシートをチェックする場合、これはあなたに起こることはありません。
初心者ロッカーは、悪名高いif
-else
コンストラクトを使用してこれを行います。
list.head
と仲間を呼び出すことは、自分自身を殺すための最良の方法の1つです。
リストの操作中にスリープします。
ガラガラヘビをlist.head
、list.head
とその友達を使わないようにできる限りのことをしてlist.head
。
head
代わりに、 headOption
メソッドを使用することをおheadOption
ます。 lastOption
メソッドも同様に動作します。 何らかの方法でインデックスにアタッチされている場合、 isDefinedAt
メソッドを使用できます。 isDefinedAt
メソッドは、パラメーターとして整数の引数(インデックス)を取ります。 上記のすべてには、忘れることができるチェックが含まれています。 あなたが意識的にそれらを省略するもう一つの理由があります。 正しい慣用的な代替手段は、パターンマッチングを使用することです。 リストが代数型であるという事実により、 Nil
を忘れることはありませんNil
とtail
呼び出しを安全に回避でき、数行のコードを保存できます。
def printRec(list: List[String]): Unit = list match { // , // n, k , . That's the power! case Nil => () case x::xs => println(x) printRec(xs) }
パフォーマンスについて少し
単一リンクリスト(SkalovList
(別名scala.collection.immutable.List
))のパフォーマンスの観点から見ると、最も安価な操作は、リストの最後ではなく先頭に書き込むことです。 リストの最後に書き込むには、リスト全体を最後まで調べる必要があります。 リストの先頭への書き込みの複雑さはO(1)からO(n)の終わりまでです。 それを忘れないでください。
オプション[リスト[?]]
Option[List[A]]
、うらやましい頻度でScalaに会ったばかりの人のコードにあります。 関数の引数と戻り値の型の両方。 多くの場合、このような傑作を作成した人は次の引数を使用します。「リストがあるかもしれませんが、代わりにnull
を使用するのは何ですか?」
さて、別の状況を想像してみましょう。 Option
は概念的に考えられる失敗した結果を表し、リストは返されたデータのセットです。 Option[List[Message]]
を返すサーバーがあると想像してOption[List[Message]]
。 すべてが順調であれば、 Some
内のメッセージのリストを取得します。 メッセージがない場合、 Some
内に空のリストを取得します。 サーバーでエラーが発生した場合、 None
を取得します。 合理的で実行可能ですか?
そしていや! システムでエラーが発生する可能性がある場合、どのエラーを知る必要があります。 このために、 Throwable
または何らかのコードを返す必要があります。 Option
はそれを可能にしますか? 実際にはそうではありませんが、 Try
and Either
はこれを支援します。
Option
ようにリストは空にすることができるため、何か問題が発生した場合に空のリストを安全に渡すことができます。 Option[List]
コンストラクトが実行可能な反例はまだ見ていません。 そのような例があればとても嬉しいですし、私と共有するでしょう。
オプション[A] =>オプション[B]
最近、 Option
別の興味深いアプリケーションに出会いました。 次の関数のシグネチャを見てみましょう。
def process (item: Option[Item]): Option[UpdatedItem] = ???
追加のコンテナを使用して変換を複雑にする必要はありません。これにより、関数の汎用性が低くなり、関数のシグネチャが視覚的に乱雑になります。 代わりに、タイプA => B
関数を使用してくださいA => B
また、元のコンテナのタイプを保存する場合は、元の結果をこのコンテナにラップし、 map
またはflatmap
map
flatmap
を使用してさらにデータを変換します。
タプル
タプル(タプル)の存在は、多くの関数型(だけでなく)言語の興味深い機能です。 関数型言語では、タプルはレコードと同じ方法で使用できます。 タプルを必要なデータで記述し、新しいタイプにラップします。たとえば、 Haskellでnewtype
を使用して、結果として、ユーザーが何も知らない実装の新しいタイプを取得します。 タプルのない純粋に機能的な言語では、どこにもありません。辞書を完全に提示できます。 それらのない革命はそれほど明白ではありません。
Erlangなどの一部の言語では、レコードはタプルよりもはるかに遅れて表示されました。 さらに、Erlangのレコードもタプルです。
Scalaはオブジェクト指向言語です。 はい、関数型プログラミング要素をサポートしています。 多くの人が私に反対すると確信していますが、Scalaではすべてがオブジェクトであることを忘れないでください。 case
クラスの存在はcase
タプルの必要性を大幅に減らします。不変のレコードを取得し、パターンと比較することもできます(これについては後で説明します)。 各case
クラスには独自のタイプがあります。
タプルは、多くの場合、オブジェクト指向言語の出身者が使用する必要があります。これらの言語ツールに興味があります。 そもそも、名前は付けられていません。
タプルが匿名のガベージダンプとして使用されない場合は、名前を付ける必要があります。
タプルを使用してデータを保存するcase class
、このためにcase class
を使用します。
機能的なスタイルの場合、前述の型のエイリアスを使用することをお勧めします(型エイリアス):
type Point = (Double, Double)
将来的には、非常に名前の付いた型を参照するようになりますが、そのようなひどいものはなくなります。
// def drawLine(x: (Double, Double), y: (Double, Double)): Line = ??? // def drawLine(x: Point, y: Point): Line = ???
Scalaでは、インデックスによってtuple要素にアクセスすることもできます。 例:
// ! val y = point._2 //
コレクションを操作しているときは特に悲しいようです:
// ! points foreach { point: Point => println(s"x: ${point._1}, y: ${point._2}") }
そして、あなたはそれをする必要はありません。 もちろん、そのような手段が読みやすさを高める例外的なケースがあります:
// rows.groupBy(_._2)
ただし、ほとんどの場合、下線付きの構文は避けるのが最善です。 一般に、彼については忘れて、覚えていない方が良いです。 Scalaには、この構文を使用しない自然な方法があります。
Scalaでは、pair._2
なしでいつでも実行できpair._2
。 そして、それを行う必要があります。
すべてがそうである理由を理解して理解するために、関数型言語に目を向けましょう。
質問 :親愛なる編集者、なぜScalaのリストインデックスはゼロから始まり、タプルは1から始まるのですか? ワシリー、ポハビンスク。
回答 :こんにちは、ヴァシリー。 答えは簡単です。なぜなら、それは歴史的に起こったからです。 SMLには、タプル要素にアクセスするための関数 #1
および#2
が存在します。 Haskell
は、タプル要素にアクセスするための2つの関数fst
とsnd
ます。
-- - . Haskell -- . . fst tuple
しかし、タプルの3番目または5番目の要素を取得するだけではうまくいきません。 信じられない? しかし、無駄に 。 そして、サンプルとの比較が最も自然であるとあなたが言うならば、それを信じないでください。 そしてHaskell
だけでなく。
Occaml
let third (_, _, elem) = elem
アーラン
1> Tuple = {1,3,4}. {1,3,4} 2> Third = fun ({_Fst, _Snd, Thrd}) -> Thrd end. #Fun<erl_eval.6.50752066> 3> Third(Tuple). 4
Python
そして、これは関数型言語ではない例です:
>> (ip, hostname) = ("127.0.0.1", "localhost") >>> ip '127.0.0.1' >>> hostname 'localhost' >>>
この知識をScalaに適用しましょう
// , trait Rectangle { def topLeft: Point ... } // val (x0, y0) = rectangle.topLeft // : points foreach { case (x, y) => println(s"x: ${x}, y: ${y}") }
match
キーワードを使用した標準のマッチングメカニズムもキャンセルされていません。
タプルは匿名のガベージダンプとしても使用でき、これは正当化される場合があります。 実際、多くの関数型言語では、関数シグネチャのレベルでサンプルと比較されています。
-- haskell -- : map :: (a -> b) -> [a] -> [b] -- -- -- -- : map _ [] = [] -- x:xs , , -- Haskell head:tail . : - cons -- :: map fun (head:tail) = fun head : map fun tail
同様のメカニズムがSMLとErlangで使用されています。 残念ながら、Scalaはそのような機会を奪われています。 したがって、タプルは、グループ化およびサンプルとの後続のマッチングに使用できます。
// Haskell, :( def map [A, B] (f: A => B, l: List[A]): List[B] = (f, l) match { case (f, Nil) => List.empty case (f, head::tail) => f(head) :: map(f, tail) }
多くの場合、タプルの一部の要素の値を更新する必要があります。 copy
方法はこれに適しています。
val dog = ("Rex", 13) val olderDog = tuple.copy(_2 = 14)
HaskellおよびSMLでの タプルの使用については、リンクをクリックして読むことができます。
実際には、タプルを使用することは(少なくともScalaで)座標を表す最良の方法ではありません。 Scalaでは、このためにcase class
クラスを使用することをお勧めします。 タプルの必要性は、主に、複合レコードを一般化された形式で折りたたむ必要がある場合のユニバーサルライブラリの存在によって決まります。 たとえば、 zip
またはgroupBy
。 したがって、タプルを使用する場合は、一般化されたアルゴリズムを記述するときにのみ使用してください。 他のすべてのケースでは、古き良きcase class
することをお勧めします。
下線について
Scalaで_
使用されているすべてのケースをリストできますか? 調査によると、これを行うことができるScala開発者はわずか7%です。 アンダースコアは、言語で何度もさまざまなコンテキストで使用されます。 これはここでよく説明されています。 ほとんどの場合、アンダースコアなしで実行することはできません。構文では、複数のインポートまたは例外のあるインポートに対してアンダースコアが必要です。 他にも合理的な用途があります(それらがなくても使用できます)。 ただし、ラムダ式内で読みやすさは追加されません。 アンダースコアが3つ以上あると、ラムダの読み取りが困難になります。
サンプルと比較すると、彼らはあなたの人生を台無しにすることもできます。 Fork
とLeaf
理解します。
def weight(tree: CodeTree): Int = tree match { case Fork(_, _, _, weight) => weight case Leaf(_, weight) => weight }
それで?
def weight(tree: CodeTree): Int = tree match { case Fork(left, right, chars, weight) => weight case Leaf(char, weight) => weight }
お気づきかもしれませんが、これらの値は使用されません。 しかし、アンダースコアで上書きする必要がある場合はそうではありません。 私を信じて、プログラミングの速度はタイピングの速度に依存しません。読みやすくするために、いくつかの余分な文字を書くことができます。
結論として
この記事では、Scalaの主要な機能イディオムについて説明し、他の機能言語と多くの類似点を引き出そうと試みました(そして、判断するのではなく、判明したかどうか)。 次の部分では、OOPとコレクションに関連するイディオムについて説明し、多くの初心者開発者を悩ませているインフラストラクチャの問題に関する私の考えを述べます。 あなたがこの記事を楽しんだことを本当に願っています。 辛抱強く最後まで読んでくれてありがとう。 継続する。