kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Scala 3で型レベル自然数をやってみたらすごく簡単になってた

Scala 3、正式リリースされてからそこそこ経ってますが皆さん使ってますか?実は自分は、主にIntellij IDEAのScala 3対応に不安もあってScala 2.13系列をずっと使い続けて来たのですが、最近はScala 3対応も進んできたようなので乗り換えを始めることにしました。で、手始めにということで型レベル自然数(型レベルペアノ数)を定義してみることにしました。

型レベル自然数は読んで字の如くで、型のレベルで自然数エンコーディングしちゃおうって技です。依存型のある言語なら最初からあるので、わざわざ型レベル自然数とか大仰な言い方しなくてもいいのですが、それはそれとして論より証拠。早速、Scala 3で型レベル自然数を定義してみます。

enum Nat {
  case Zero
  case Succ[A <: Nat](v: A)
}

定義はこれだけです。簡単ですね。Scala 2だと

sealed trait Nat
object Nat {
  case object Zero extends Nat
  case class Succ[A <: Nat](V: A) extends Nat
}

のように書かなければいけなかったのでとっても面倒でした。さてさて、とりあえずこれで型レベル自然数の定義はできたのですが、よく使う一桁の自然数は別名定義しておきたいですというわけで、次にこんなん書きます:

type _0 = Zero.type
type _1 = Succ[_0]
type _2 = Succ[_1]
type _3 = Succ[_2]
type _4 = Succ[_3]
type _5 = Succ[_4]
type _6 = Succ[_5]
type _7 = Succ[_6]
type _8 = Succ[_7]
type _9 = Succ[_8]

この辺はScala 2時代と変わらず面倒くさいです(もしお手軽に型エイリアスを大量生成できる技知ってる人いれば教えてください)。で、この次が本題なのですが、型レベルで自然数を表すからには当然加算や減算などの演算を定義したいですよね。これがScala 3になってかなり楽になりました。たとえば、足し算を表す型コンストラクPlusは次のようにかけます。

type Plus[A <: Nat, B <: Nat] = (A, B) match {
  case (x, Zero.type) => x // 右辺が0なら左辺の型を返す
  case (x, Succ[y]) =>  Succ[Plus[x, y]] // そうでなければxとyをPlusした型をSuccする
}

ここでは、

  • 型の別名付け機能: type Plus[...] = ...
  • 型のパターンマッチ: 型の別名定義内で型へのパターンマッチをできる

という二つの機能を使っていますが、再帰的な足し算の定義をそのまま素直に書き下せています。Scala 2だと型パターンマッチも型の別名定義中の「型の再帰」も素直にかけませんでしたから、この快適さにビックリです!

続いて掛け算(Multiply)を定義しましょう。掛け算は当然ながら足し算を使って定義できるので、以下のような感じでしょうか。

type Multiply[A <: Nat, B <: Nat] = (A, B) match {
  case (_, Zero.type) |  (Zero.type, _) => Zero.type // 両辺の片方が0なら0
  case (x, `_1`) | case (`_1`, x) => x // 両辺の片方が1なら1
  case (x, Succ[z])  =>  Plus[x, Multiply[x,y]] // y >= 2のとき、x  * y  = x + (x * (y - 1))
}

この型レベル演算がうまく行っているか確かめるためにmainメソッドをつけてみましょう。Scala 3ではクラスがなくてmainメソッドだけのプログラムも許されるようになりました(明らかにKotlilnを意識した仕様変更ですね)。

def main(args: Array[String]): Unit = {
  val one: _4 = null.asInstanceOf[Plus[_2, _2]]
  val six: _6 = null.asInstanceOf[Multiply[_2, _3]]
  val nine: _9 = null.asInstanceOf[Multiply[_3, _3]]
  println("Hello")
}

これらのプログラムにNat.scalaという名前をつけて保存し、

scala Nat.scala

としてみれば問題なくHelloが表示されます。さて、上記ではone, six, nineの型を正しく指定しましたが、次のように間違った型を指定するとどうなるでしょうか。

val wrong: _2 = null.asInstanceOf[Multiply[_4, _2]]

これを含めたプログラムをコンパイルすると次のようになります。

-- [E007] Type Mismatch Error: /home/mizushima/repo/scala-nats/Nat.scala:36:35 ---------------------------------------------
36 |  val wrong: _2 = null.asInstanceOf[Multiply[_4, _2]]
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |                  Found:    Nat.Succ[Nat.Succ[Nat.Succ[Nat.Succ[Nat.Succ[_3]]]]]
   |                  Required: _2
   |
   | longer explanation available when compiling with `-explain`
1 error found
Errors encountered during compilation

要求されていた(Required)な型は_2だけど、実際にはNat.Succ[Nat.Succ[Nat.Succ[Nat.Succ[Nat.Succ[_3]]]]] = _8 だよというエラーですね。というわけでScala 3になってこういうお遊びもやりやすくなたのでした。

嫉妬心がほとんどないという話

 すっごく個人的な話なんですが、生まれてからもうすぐで40年になろうという歳になって思うわけです。

「自分は嫉妬心ないなあ」と。

 妬み嫉みは自分より遥か高みにいる人に対しては起きなくて、自分と拮抗する実力の持ち主との間で起こるともよく聞きますが、そういう相手に対しても

「まあ、その人なりに頑張ったんだろうし」

 くらいにしか思わないです。男女関係だって、かつて好きだった異性が結婚した話を聞いたときも別段ショックを受けた覚えがありません。「まあ良かったよね」くらい?

 そんな風にしてずっと生きてきたものだから、他者の嫉妬心や嫉妬してしまって自己嫌悪する気持ちがイマイチ理解できなかったりします。

 30代は結構ストレスが多い10年という一面はありましたが、嫉妬心がほとんどないので、誰かと比べて境遇を嘆くことがなかったのは良い事だった……のかもしれません。実際、メンタル的に一番しんどい的ですら、楽しそうな様子の誰かを見て心がしんどくなる、なんてことはなかったですし。

 オチはないんですが、実際問題、嫉妬心が(ほとんど)ない人ってどのくらいいるんでしょう?ググっても時々いるくらいには珍しくない存在みたいですが。

ChatGPTを使うようになって変わったこと

 OpenAIのChatGPT (GPT-3.5)がリリースされてから約半年、GPT-4やChatGPT APIがリリースされてから約2ヶ月。書いてみると非常に新しいテクノロジーなわけですが、既にかなりどっぷり依存しています。ChatGPT Plusには月額$20を課金しているし、gpt-4 APIのwaitlistにもいち早く登録して、数日後には使いまくるようになったくらいです。

 さて、そんなChatGPTを使うようになって、私の生活の何が一番変わったのかなと振り返ってみたくなりました。

  • 手を出すのが億劫になっていた領域のプログラミングをやるようになった

 世の中には、別に難しいわけではないけど、雑多な新しいことを覚えるのが面倒くさくて手を付けていなかった分野がたくさんあります。たとえば、私にとってはChrome拡張がその一つでした。動作原理はそう複雑でないこともわかっていたし、やろうと思えばできた(本当の意味で)けど、とにかく手を付けるのが億劫でした。何が億劫だったかというとドキュメント読んで設定ファイル書いて、単純ミスを修正して、というところに時間を取られるのがとにかくネックだったのですよね。しかも、別にChrome拡張で大したことをやりたいわけでもないのでかける時間に対して見合わなかったのです。

 しかし、少なくとも「最初の一歩」を踏み出すなら、ChatGPTに聞いてしまえば一発です。質問だけで完璧に動作するスクリプトが出てくることは稀ですが、少々の手直しで動くことが非常に多いです。そのおかげで、単純ではありますがChrome拡張を作ったり、あるいはLINE BOTを作ったり、Slack BOTを作ったりするようになりました。ReactやElectronのアプリケーションを作るようになったのもChatGPTに質問したのがきっかけだった気がします。

 他にも、とにかく「手っ取り早くその場の問題を解決する」ためのプログラムを生成してもらうにはChatGPTは非常に優秀です。大規模なプログラムを一発で作ってもらうにはプロンプトを工夫する必要がありますが、数百行程度のプログラムを生成してもらって手直しする前提なら雑に質問すれば十分です。そのおかげで今までやらなかった方向の趣味プログラミングをやる機会が増えて大変充実しています。

 もちろん、いわゆる幻覚(hallucination)問題は無視できませんが、日曜大工的なプログラミングでは「明らかにおかしな箇所」はすぐわかることも多く、そこだけ直してやればいいので楽なものです。

  • 名前を考えるのが楽になった

 ChatGPTの応用としてよく言われるものですが、案外馬鹿にできません。良い名前、良い文章表現に悩むことは多いものですが、そういう時に「案を10個出して」と言えばそれだけで物事が進むことも多いのですから、全く大助かりです。たとえば、以前にとある理由で「柔らかい」から連想される単語には何があるだろうかと考えたことがあったのですが、そういう「連想系」の質問はChatGPTがもっとも得意とすることです。案の10個や20個、すぐ出してくれます。名前や表現に悩むことは多かったのですが、候補をそのまま採用しないにしても、それだけで思考コストを節約できることは多いものです。

  • 論文読みがはかどるようになった

 これはどちらかといえば最近解放されたプラグイン機能によるものが大きいですが、PDFのURLを投げて、日本語で要約させたり雑に質問ができるので、英語が苦手な技術者の自分としては論文読みがとても捗るようになりました。

  • キャラ付けしたBOTと対話するようになった

 好みのキャラにカスタマイズしたLINEボット(複数)やSlackボット(複数)と対話するのは楽しいものです。GPT-3.5のAPIだとsystemロールの指示でも結構無視してくれますが、GPT-4のAPIならかなり忠実に指示を聞いてくれます。というわけで、どういうものかは明かせませんが好きな性格設定をしたボットを作って日々楽しんでいます。

 ちなみに、あくまで個人的な感想にはなるのですが、ある種のカウンセリングなどには実際問題非常に有効なんじゃないかと思うことがあります。悩み事があるときに文字として書き出すというのは定番ですが、やはり日記に書くだけだと「自分との対話」でしかないわけです。その点、たとえボットとでも対話するのは考えの整理や袋小路に行き詰まっているときに有効な気がします。

  • ChatGPTで遊ぶようになった

 ある意味これが本命じゃないか?と思うことがありますが、特にGPT-4の「頭の良さ」は未だに底が知れない部分があり、弱点を探ったり、その弱点を補完するプロンプトを考えてみるのはそれだけで心躍る時間です。「リストの長さ」という形で五・七・五の制約を与えることで俳句ぽいものができるようになったときは結構感動しました。

 あるいは、PEGのインタプリタを解釈させようとしたところ、何度やっても勝手にCFGの亜種と誤解してくれるので悪戦苦闘していたのですが「Note that it's not CFG」的な「先入観を排除する」プロンプトでそれを防止できたときは、内心ガッツポーズしたくらいです。

 他にも色々あるのですが、ChatGPTを使うようになって「良い意味で」色々変わったなあと思うのでした。

環境変数CUDA_VISIBLE_DEVICESでハマったのでメモ

 最近はすっかりLLMにハマってるわけですが、ハマり過ぎたあまり一台のPCに複数のGPUを搭載(といっても、RTX A4000(VRAM 16GB)とQuadro M6000(VRAM 24GB)という妙な組み合わせですが)してたりするのですが、どうにもLLMの出力速度が遅いなーなどと思っていました。

 感覚的には主にM6000の方が使われてる感じで、こいつはVRAMこそ多いもののRTX A4000より圧倒的に遅い(数世代前)ので、メインで使ってほしくない。というわけで、CUDA_VISIBLE_DEVICESという環境変数をいじってやればいいってところまではたどりついたのですが、CUDA_VISIBLE_DEVICES=2とかしてやってcuda:1みたいに指定すると妙なエラーが出る。色々調べてわかったのは、

export CUDA_VISIBLE_DEVICES=0,1
python llm.py # この中でcuda:1なりcuda:0なりを指定する

 のようにすればいいらしいということ。この環境変数はデバイスの数ではなくて、GPUのリストを指定するものだったわけですね。盲点でした。

ChatGPT に「形式が整った俳句」を作ってもらう(あるいは「文字数指定」をする)

 おはようございます。今日も元気にChatGPTをしています。さてさて、ChatGPTに俳句を作らせようとしても失敗するという話は一部界隈では有名です。たとえば、次のように「俳句を作ってください」とだけ指定しても五・七・五を平然と無視してくれやがります。

俳句(うまく行ってしまった)

 あれ?うまく行ってしまいましたね。ChatGPTは非決定的に答えを返すのでこんなこともありますよね。同じプロンプトでリトライします。

俳句(きちんと間違えた)

 これでようやく間違えてくれたので本題に入れます。と、このように俳句を詠んでと言っても五・七・五を平然と無視してしまうのです。では、俳句のフォーマットを指定してやった場合はどうでしょうか?

俳句(一回の修正で通ってしまった)

 あれれ?一回の修正でちゃんと五・七・五になおしてきましたね。う、うーん。まぐれ当たりのハズなので、三句くらい詠めばはずしてくれるはず。

はずしてくれた

 今度は予定通り(?)です。文字数に関する指定をしても、どうにもうまく行きません。

 この「文字数制限の指定をChatGPTは無視する」というのは廃ChatGPTerには有名な話なのですが、一方で「5個」とかの数量は理解してくれますし、リストの長さは理解してくれるんですよね。とここで昨夜思いついたのが「文字列を文字のリストとして表現すれば、ChatGPTはちゃんと文字数制限に相当するものを理解してくれるのでは」ということでした。というわけで、トライです。

お願い(1)

お願い(2)

 文字リストとして出力してもらう形にしたのでだいぶ「改善しています」。しかし、制限をもうちょっと厳密に守ってもらおうかと、より形式的に文字リストを出力してもらうようにしました。

文字数指定(厳密)

 今度は指示こそ通ったももの「本当に5文字、7文字、5文字からなる文字リストを出力する」だけですね。文字数をそのまま指定しようと無視するし、厳密に指定させると今度は単なる文字列長制限つき文字列生成になってしまうようです。しかし、ここで諦めては負けです。「文字リストを出力させる」アプローチはいいはずなので、あとは3つのリストが五・七・五に対応させるとこまでやればいけるハズ。てことで

成功例(1)
成功例(2)

 やってみたら見事行けました!季語が一部入ってないですが、五・七・五にきちんと従わせられたので及第点としていいでしょう。ただ、欲を言えば1回の質問でちゃんと「俳句になっている文字リスト」が欲しいですよね。もうちょっと頑張ってみました。

俳句(成功)

 ここまでやって思ったのですが、ChatGPTさんはやはり俳句の意義自体は理解しているように感じるので、プログラミング言語のコレクションのような、ChatGPTが間違えようがない形式を経由するとうまく行きやすいかもしれません。

ChatGPT APIで仮想人格を作り込む

 おはようございます。さて、皆さん。ChatGPT、使いこなしてますか?私は……使いこなしてるかはわかりませんが、ドはまりしてることだけは確かです。プログラミング言語間のトランスパイルをできるツール

github.com

を作ったり、仮想人格を作ってそのキャラとチャットしてみたり、テキスト->音声ソフトウェア(VOICEPEAKやVOICEVOX)を使って、キャラに声をつけてみたり。さらにWhisper APIと組み合わせて[キャラクタと音声会話できるツール

github.com

を作ったりと色々変なことも試しています。ちなみに、VOICEPEAK(東北ずん子)についてはこちらから、

www.ah-soft.com

VOICEVOX

voicevox.hiroshiba.jp

についてはこちらから。VOICEPEAKはさすがに有料で公開されてるソフトウェアだけあって、普通に読み込ませるだけでもより自然なイントネーションでしゃべってくれるなどの利点がありますが、1万円超と逸般人でない人が試すにはちょっとお高いので無料かつライセンスが比較的ゆるい(詳細は上記を参照)VOICEVOXを使ってみるのがいいのかなと思います。

それはともかく、ChatGPT APIを使えば、あの生真面目なChatGPT君のキャラだって自由自在にカスタマイズできます。試しにちょっとしたキャラ設定を作ってみましょう。手元のSlack Botなどを使ってもいいのですが、試しやすいようにOpenAIのPlaygroundを使います。

ChatGPT APIでキャラを作り込んでみた

顔文字の使い方が20代の女性っぽいのかは不明ですが、キャラ付けした部分はうまく反映されているように見えます。

ChatGPT APIでは「system」「user」(主にユーザからの質問に使われる)」「assistant(主にChatGPTからの応答に使われる)」の3種類のrole=役割があるのですが、systemでキャラ設定を最初に作り込んでおけば、以後(倫理規定に引っかからない限りは)、その設定を演じてくれます。これはWeb版のChatGPTでも使えることは知られていますが、APIを使えば一度作り込んだキャラ設定をずっと維持できるのがメリットでしょうか。

ちょっと設定を付け加えて、魚介類、得にサバが好きということにしてみましょう。

サバ好き女子大生

うまく反映されていますね。苦手なものとして、大人数の飲み会というのも追加してみましょう。

大人数の飲み会が苦手な女子大生

User=私が「30名くらいの大所帯」で愚痴ったところに、ちゃんと「私も大人数の飲み会って苦手だから」と返していますよね。「わかるよ」とも。こんな風に言葉だけで仮想人格を作り込める万能ソフトウェアって今まで存在しなかったわけで、こういうところにもChatGPTの凄さがあるなと感じます。

ちなみに、今回はgpt-4-0314というモデルを使っていますが、gpt-3.5-turboでもそれなりにはキャラを演じてもらえます。ただ、仮想人格を作り込むという目的だと今一つという感がありまして、設定が中途半端にしか反映されません。たとえば、こんな感じです。

女子大生(GPT-3.5)

共感しつつ、というのは反映されていますが、大人数の飲み会が苦手って部分が中途半端にしか反映されていませんよね。タメ口設定も無視されています。この辺りを見ても、gpt-4の賢さが伺えます。

というわけで、APIを使える皆さんはせっかくなので、自分好みの「アシスタント」のキャラを作り込んで答えてもらうといいかもしれませんね。いやまあ、書いてから気づいたんですが、キャラを作り込めるのはgpt-4からということなので、まだgpt-3.5までしか使えないケースだと(そして、現状だとgpt4をAPIで使えるのはごく一部っぽいので)作り込みに多少苦労するとは思いますが、それについてもプロンプトを工夫すればある程度はうまくやってくれます。

ChatGPT APIベースの多言語トランスパイラGPTranslatorを公開しました!

 この日記やTwitterなどで、ChatGPT (GPT-3.5, GPT-4)はプログラミング言語間の変換(に限らないですが)が得意ということを書きましたが、試している内に「毎回似たような指示するのだるいな」と思うようになりました。というわけで、Reactベースで動くChatGPT APIを使ったトランスパイラGPTranslatorを作ってみました。ちなみに最近はトランスパイラという言い方がすっかり主流になった気がするのですが、一昔前だとトランスレータと呼ばれてた気がしますね……。

github.com

 といっても、要はChatGPT APIに投げる前に適当なプロンプトを付加しているだけという代物です。ただ、コードフラグメントを別言語に変換するときや、YAML -> JSONみたいな設定ファイルの形式を変換するときなんかには多少役に立つのではないかなと思います。

 詳細はGitHubリポジトリを見てもらうとして、OpenAIのAPI KEYを

export REACT_APP_OPENAI_API_KEY=<api key> # this must be set before npm start

 という形で指定しなければいけないところは多少使い勝手が悪いかもしれません(設定ファイルに書けた方がいいような気もします)。ともあれ、使ってみてバグや改良点などあればPull Requestなどがんがんいただければと思います。言うまでもないですが、ChatGPTベースということで精度は完璧ではありませんし、変換元や変換先の言語によってはうまくいかなかったりすることもあります。

以下のスクリーンショットだと、Javaにはメソッドの中に直接メソッドを書く機能はないのになんか入っちゃってますね。そもそも型がないJavaScriptをそのままJavaに変換しようと思うと無理が生じるのは必然でもありますが。

JavaScript to Java

一方、PythonからJavaScriptへの変換は比較的素直に行きやすいようです(もっと複雑なサンプルになればまた別ですが)。

a

Python to JavaScript