GPTさんに英語の勉強を手伝ってもらう - Function Callingのパースエラー対策

ChatGPT、いろいろ教えてくれますね。
じゃあ英語の勉強用に文章やら確認問題やら作ってもらうといいじゃない、ってやってみたら、よさそう。

日本語の文章を入れると、いい感じの単語数の文章で英訳して、難しい単語とその説明を挙げて、確認問題をつくってくれます。

こんな感じのプロンプトで、%sのところに日本語文が入ります。

You are English tutor. The student has vocablary level with 2000 words.
Make a English text with around 200 words by translating and summerizing the text below.
In addition, choose five difficult words from the English text and make three comprehension questions.
You must use the function `english_text`
---
%s

で、Function Callingでいろいろ設定してもらうんだけど、english_text(String text, String[] words, String[] wordDescriptions, String[] questions)という関数を指定しているイメージ。

{
  "model": "gpt-4-0613",
  "messages": [
    {"role": "system", "content": "%s"}
  ],
  "functions": [
    {
      "name": "english_text",
      "description": "Set the English articles to learn English",
      "parameters": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "The English text to learn"
          },
          "words": {
            "type": "array",
            "description": "5 difficult words in the text",
            "items": {
               "type": "string"
            }
          },
          "word_descriptions": {
            "type": "array",
            "description": "descriptions about the difficult words",
            "items": {
               "type": "string"
            }
          },
          "questions": {
            "type": "array",
            "description": "3 complehension questions about the text",
            "items": {
               "type": "string"
            }
          }
        },
        "required": ["text", "words", "word_descriptions", "questions"]
      }
    }
  ]
}

ここで、配列を指定するときはtypearrayにして実際の型はitemsに設定するところがミソですね。

"words": {
  "type": "array",
  "description": "5 difficult words in the text",
  "items": {
     "type": "string"
  }
},

そうすると、argumentsにこんな感じのJSONが返ってきます。。。ってオイ、textのなかのダブルクオートがちゃんとエスケープされてないやん!というのが見どころ。

{
  "text": "\"Prohibition of object-oriented programming because it makes programs unreadable\" has ... object-oriented programming to some extent". Object-oriented programming should not be the goal.\"",
  "words": ["prohibition", "unreadable", "mazes", "scattered", "branching"],
  "word_descriptions": [
    "a law, rule, or order that forbids something",
    "difficult or impossible to read",
    "a confusing network of paths or passages",
    "spread out or scattered in various directions",
    "the act of dividing into branches or the state of being divided into branches"
  ],
  "questions": [
    "What is the meaning of 'prohibition' in the text?",
    "What does it mean for code to be 'scattered'?",
    "How would you define 'branching' in programming?"
  ]
}

鍵カッコが原文に入っていると、まず間違いなく失敗する感じなので、GPTに投げなおしリトライやってるとそれだけですぐに10円くらいかかってしまいそう。2Kトークンくらいあるので、一回0.5円。あと、30秒くらいかかるので、4回リトライしてたら2分とかかかって、待ってられません。
ということで、JacksonとかJSONパーサーを使わず自力でパースします。

argumentsをとるところはJacksonでもいいのだけど、そこは難しくないのでこんな感じで取れます。

var arguments = json.lines().map(String::trim)
        .filter(s -> s.startsWith("\"arguments")).findAny()
        .map(s -> parseText(s, "arguments"))
        .orElseThrow(() -> new NoSuchElementException(
                "arguments is not found. it may not be function_call"));

parseTextは最初と最後のダブルクオートとかを取るだけ。

static String parseText(String line, String name) {
    line = line.trim();
    var len = line.length();
    var text = line.substring(name.length() + 5, len - 1 - (line.endsWith(",") ? 1 : 0));
    return text.translateEscapes();
}

パラメータに配列がなければ、argumentsの中身も同じ感じで取ってこれるのだけど、配列は一行にまとまってたり要素ごとに改行されてたり、ちょっとめんどう。

var article = new Article();
String buf = null;
for (String line : arguments.lines().toList()) {
    line = line.trim();
    if (line.endsWith("[")) {
        buf = line;
        continue;
    }
    if (buf != null) {
        buf += line;
        if (line.endsWith(",")) {
            buf += " ";
        }
        if (!line.startsWith("]")) {
            continue;
        }
        line = buf;
    }
    if (line.startsWith("\"text")) {
        article.text = parseText(line, "text");
    } else if (line.startsWith("\"words")) {
        article.words = parseTextArray(line, "words");
    } else if (line.startsWith("\"word_descriptions")) {
        article.wordDescriptions = parseTextArray(line, "word_descriptions");
    } else if (line.startsWith("\"questions")) {
        article.questions = parseTextArray(line, "questions");
    }
}

parseTextArrayはこんな感じ。

static List<String> parseTextArray(String line, String name) {
    line = line.trim();
    var len = line.length();
    var text = line.substring(name.length() + 6, len - 2 - (line.endsWith(",") ? 1 : 0));
    return Arrays.stream(text.split("\", \""))
            .map(String::translateEscapes)
            .toList();
}

GPT-3.5だと、「200単語くらいで」っていうのを聞いてくれないので、一度翻訳だけさせて要約するといいと思います。GPT-4は内容はいいんだけど、function callしてくれないことが多い気がした。
というか、あれ?Chat APIはGPT-3.5とGPT-4は同じ?分量長くなるとGPT-3.5でもGPT-4でも同じくらい時間がかかるので、そうするとGPT-4でよさそう。

コードはこれ。
JSONを自力で解析したので、Jacksonなどの依存なしに標準APIだけで動きます。
https://gist.github.com/kishida/b473e1aabbb375cead2a9e5a7306ea9a

Dependency Injectionでやりたいことはモジュールimport

Dependency Injection(DI)、最近のフレームワークには欠かせない気がする機能になってますね。
そしてDIの説明をみると「依存性の注入」みたいなことが書いてあって、ようわからんになりがちです。
実態としては高機能なimportなので、あまり難しいことを考えなくていいような気がします。

たとえば、こんな感じのMyServiceクラスがあってDIコンテナに管理させるとします。

@Component
class MyService {
  void method() {
  }
}

そして、MyServiceを使うMyControllerがあるとします。

@Component
class MyController {
  @Inject
  MyService service;

  void hello() {
    service.method();
  }
}

これって、実際のところMyServicemethod()がstaticであれば単にimportでいいわけです。

class MyService {
  static voic method() {
  }
}

のようにして、こう。

import MyService as service;
class MyController {
  void hello() {
    service.method();
  }
}

ただし、Javaのimportでは別名はつけれないので、こう。

import MyService;
class MyController {
  void hello() {
    MyService.method();
  }
}

実際、「staticキモチワルイ」を解消するくらいの気持ちで使ってることも多いです。

ただ、Javaの場合は先ほどの別名も含めて、いろいろ言語機能が足りていないのでDIというトリックで不足分を解決している、という感じになっています。 不足しているものとしてあげられるのが次のようなものになります。

  • 動的スコープ
  • AOP
  • テスト時のモック化
  • 設定外部化
  • 別名

動的スコープは、requestだとかsessionだとかでオブジェクトの寿命を指定する機能です。
これはこれでThreadLocalをいい感じに隠蔽する機能でもありますね。

AOPは、トランザクションとかロギングとかをメソッドに噛ませる機能ですね。「オブジェクト指向の次はアスペクト指向じゃー!」って盛り上がって、言語機能として使えるAspectJとかが一瞬流行ったことがあるけど、結局トランザクションとロギングくらいしか使いどころがないことがわかって、表だって話題に出ることはなくなりました・・・

あと、テスト時のモック化。
テストのときにDBやらファイルやらネットにアクセスしてられないということで、実装を入れ替えることができますね。
ただ、これはクラスローダーの問題なので、staticにして実装を入れ替えることは可能といえば可能。

ついでに設定外部化。
DIするついでに、いろんな値を設定から持ってこれるようになってますね。

あとは別名。やってもよさそうなのだけど、現実みんなSpring使ってて代替されてるのと、パターンマッチングなんかの機能の実装で忙しそうなので、いまのところは気配なしですね。言語機能おちついたらやるかも、という期待をしている。

という感じで、言語機能がそれなりに整えばDIは不要にできるものです。AOPAspectJで実現できるように。
「依存性の注入」みたいなことがやりたいわけではなく、付随する便利さのために使うものですね。
オブジェクト指向とセットで説明されることも多いけど、言語機能の不足をオブジェクト機能+いろんなハックで解決してるというものでもあります。

そういえばWEB+DB PRESS vol.132の特集にもそういう話を書いたのだった。

OpenAIのFunction Callingを使って自然言語でツールの操作をする

先週、OpenAIから、APIでの返答に関数呼び出しのパラメータを返してくれるFunction Callingが発表されました。
試してみると結構たのしかったのでまとめてみます。

解説動画はこちら
youtu.be

とりあえず、こんな感じ。

用意したのは3つの関数。

set_position(id, left, top)
set_size(id, width, height)
set_color(id, color)

で、その割に、「中央に」だとか「隣に」だとか、コンテキストを踏まえて座標などを計算して関数を呼び出してくれています。
また、「四角も同じ」で直前に指示した「三角を緑に」と同様、四角を緑にしてくれています。

ほんとはちゃんとエディタ的にやろうと思ったけど、そこまでする必要ないなってなったので、表示のみで。

Function Callingとは

Function Callingは、手持ちの関数を教えておいてあげると、プロンプトの内容に従って適切な関数とパラメータを返してくれるというものです。
プラグインの利用時にChatGPTがやってることをAPIからも使えるようになった、という感じですね。

OpenAIの発表はこちら。
https://openai.com/blog/function-calling-and-other-api-updates

ドキュメントはここ。
https://platform.openai.com/docs/guides/gpt/function-calling

APIリファレンスはここ。
https://platform.openai.com/docs/api-reference/chat/create#chat/create-functions

リクエストの生成

では、OpenAIに投げるリクエストの、通常のチャットと考え方が違う部分について書いておきます。

関数定義

こんな感じの形式で、こちらの手持ちの関数を登録します。ここで、結局はパラメータのパース処理などを自分で行ったりするので、実際に実装された関数にこだわる必要はなく、あとでパースしやすい形式でパラメータを受け取るようにすればいいと思います。

"functions": [
{
  "name": "set_position",
  "description": "Set the position of an object",
  "parameters": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "The object ID to move"
      },
      "left": {
        "type": "integer",
        "description": "The left position in pixels"
      },
      "top": {
        "type": "integer",
        "description": "The top position in pixels"
      }
    },
    "required": ["id", "left", "top"]
  }
},

システムメッセージ

roleにsystemを設定したメッセージには、現状の状態を設定します。 こんな感じのプロンプトになっています。

You are object manipulator. 
field size is 800, 600. 
we have 3 objects below.
 id:rect, left:300, top:50, width:150, height:100, color:red
 id:image, left:250, top:240, width:240, height:160, color:black
 id:triangle, left:600, top:200, width:170, height:150, color:blue

全体サイズを指定することで「画像を中央に」などが通るようにしています。そうすると、オブジェクトのサイズも考慮した位置をset_positionで渡してくれます。 また、現在のオブジェクトの状態を渡しています。ChatGPTで対話するときに理解してくれるよなって思える程度の記述で十分です。

対話履歴

コンテキストを反映した指示ができるよう、履歴を送るようにします。

{"role": "user", "content": "三角を左に"},
{"role": "assistant", "content": "I moved the triangle from (600, 200) to (300, 200)"},
{"role": "user", "content": "少し戻して"},
{"role": "assistant", "content": "I moved the triangle from (300, 200) to (550, 200)"},
{"role": "user", "content": "三角を緑に"},
{"role": "assistant", "content": "I changed the triangle color from blue to green"},
{"role": "user", "content": "四角も"}

ここでuserの指示だけの履歴でいいかなと思ったけど、それを全部新しい指示だと思ってる感じだったので、システムが返してきた操作を保持してます。

※ 追記 21:01 コメントに、こういう場合のroleにはfunctionが用意されているという指摘がありますが、function callをそのまま記録すると直前の状態が保存されないため、「少し戻して」が失敗しやすくなります。
また、これはあくまで履歴なので、何が新たに生成されるかについては、そこまで大きな影響はないはずです。
当初rorleをsystemにしてたけど、assistantにしました。

レスポンスの処理

レスポンスのうち、メッセージ部分はこんな感じで返ってきます。ここにfunction_callが含まれる。

{
  "index" : 0,
  "message" : {
    "role" : "assistant",
    "content" : null,
    "function_call" : {
      "name" : "set_position",
      "arguments" : "{\n  \"id\": \"triangle\",\n  \"left\": 800,\n  \"top\": 200\n}"
    }
  },
  "finish_reason" : "function_call"
}

argumentsJSON文字列なので、改めてパースするのがめんどいという程度。 こんな感じで関数名で分岐してそれぞれの処理を書いていけばOKです。

switch(functionCall.at("/name").asText()) {
    case "set_position" -> {
        var oldLeft = obj.getLeft();
        var oldTop = obj.getTop();
        // オブジェクトを移動
        obj.setLeft((int) args.get("left"));
        obj.setTop((int) args.get("top"));
        history.addLast(new ChatLog("system", "I moved the %s from (%d, %d) to (%d, %d)"
                .formatted(obj.getId(), oldLeft, oldTop, obj.getLeft(), obj.getTop())));
    }

ここで、返答のときに変更前後の状態を含めたログを履歴に保持しています。そうすることで「元に戻して」などの処理の確度をあげています。

GPT-3.5とGPT-4の違い

モデルにはgpt-3.5-turbo-0613gpt-4-0613が選べます。
3.5のほうがかなり速いのだけど、GPT-4はコンテキストがある指示についてのハズレが少ないです。
直接的な処理は3.5でも大丈夫だけど、「少し戻して」のような履歴と位置関係が関係あるものは失敗率が高い感じ。
なので、できればGPT-4を使いたいけどレスポンス時間と利用価格がなぁというトレードオフになりそう。

まとめ

こうやって遊ぶ分には面白いです。プログラミングも難しくないです。
でも、実際に一般に使ってもらう機能と考えると、かなり難しいです。作る側は何ができるか把握しているけど、使う側はノーヒントなので、できることをやらない、できないことをやろうとするという感じになりそう。
ただ、たとえばカレンダーやタスク管理など機能が多いサービスではかなり便利に使えるはず。あと、ChatGPT使うからって一発逆転することはなくて、持ってるサービスがより便利になるという感じになりそう。
LLM自体がアプリケーション的にはナビゲーションとしての役割が多くなりそうだったけど、Function Callingはそれが作りやすくなった感じ。
けどなんか、対応する範囲が見えないとか何ができるか開発者自体にも把握できないとか、オープンワールドFPSを作るのと同じ種類の苦労がありそう。
あと、実際には操作対象はかなり多いのと言葉で対象指定は難しいので、ツール側で選択して操作を指定することになると思います。またそのとき状態報告のためのプロンプトはかなり長くなります。可能な操作も多くなるため、1つの関数をなるべく高機能化するほうがよさそう。今回だと、set positionとset sizeはset geometoryみたいにして1関数にまとめるとか。

今回のソースはここです。
https://gist.github.com/kishida/95e661db414bb7b05ec30b8d1b395d17

ChatGPTは虚構新聞を知らない

ChatGPTは何でも知ってますよね。で、虚構新聞なんて話題になってることも多いし、当然しってるだろうと思ったら、知りませんでした。

これ、国際信州学院大学を知らなかったので、虚構新聞も知らないんじゃないかと思ったんですよね。

こういったフェイク系ジョークサイトは、言語モデルの学習にかなり邪魔になると思うので、名指しで外されているんではないかという気がします。

ところで、国際信州学院大学しらなそうなので、ホームページあるよみたいなことを言うと、「知ってるんなら自分で調べろや、ボケ」みたいなことを非常に冷静に丁寧に言われました。
ChatGPTは静かにキレますね。

CTranslate2でRinnaモデルをコンバートしてCPUや8GB GPUで動くようにする

CTranslate2はTransformerモデルをCPUやGPUで効率的に動かすライブラリです。
https://github.com/OpenNMT/CTranslate2

CTranslate2の機能のひとつにモデルの量子化があります。INT8で量子化すると雑に必要メモリが半分に。そしてCPUでも動かしやすくなるので、GPUなくてもLLMが試しやすくなります。

まあ、INT8を使うだけだと、モデルの読み込み時のfrom_pretrainedload_in_8bit=Trueをつければいいのだけど、これがbitsandbytesというライブラリを使ってて、そしてbitsandbytesがWindowsに対応していない。*1

一応、有志がビルドしたDLLを使えば動くのだけど、めんどい。
https://qiita.com/selllous/items/fbaa2c3d2d504e436b17

CTranslate2だとちゃんとWindowsで動くので安心。

とりあえずpipでctranslate2をインストール。

> pip install ctranslate2

変換

そうするとct2-transformers-converterでHugging Faceのモデルが変換できる。

> ct2-transformers-converter --model rinna/japanese-gpt-neox-3.6b-instruction-ppo --quantization int8 --output_dir rinna-ppo-ct2 

このとき、出力先のフォルダが存在していたら怒られます。--forceをつけましょう。

モデル名はHugging Faceに行ってコピーするのが確実。
https://huggingface.co/rinna/japanese-gpt-neox-3.6b-instruction-ppo

ちょっと待つと変換できます。1パラメータ8bitなので、パラメータ数GBのファイルができる感じです。

変換時にはメモリを食って26GBくらい使うっぽいので、32GBほしいところ。 と思ったら--low_cpu_mem_usageというオプションを見つけたので、これをつけると16GBメモリでいけそう。
メモリ消費をおさえると時間かかるかなと思ったけど、そんなこともなかった。

動かす

RinnaモデルはGPT-NeoXアーキテクチャなので、このソースが使える。
https://opennmt.net/CTranslate2/guides/transformers.html#gpt-neox

tokenizerの読み込み時にuse_fast=Falseをつけたり、トークナイズ時にadd_special_tokens=Falseが必要だったりで、こんな感じに。

import ctranslate2
import transformers

model_name = "rinna/japanese-gpt-neox-3.6b-instruction-ppo"
ct2_model = "rinna-ppo-ct2"

generator = ctranslate2.Generator(ct2_model)
tokenizer = transformers.AutoTokenizer.from_pretrained(model_name, use_fast=False)
prompt = "ユーザー: 日本の首都はどこ?<NL>システム :"
tokens = tokenizer.convert_ids_to_tokens(tokenizer.encode(prompt, add_special_tokens=False))

results = generator.generate_batch(
    [tokens],
    max_length=256,
    sampling_topk=20,
    sampling_temperature=0.7,
)

text = tokenizer.decode(results[0].sequences_ids[0])
print(text)

GPUで動かす

generatorの読み込み時にdevice='auto'か、明示的にdevice='cuda'とすればGPUで動きます。

generator = ctranslate2.Generator(ct2_model, device='auto')

けど、ここでCUDAのバージョンが11.xである必要があります。
12.1と11.2が入っていたので、11.2がPATHで最初に来るように変更。

しかし、なんかcublas64_11.dllがないというエラーが。

torch/libにはcublas64_11.dllがあるのに不思議とおもいつつ確認してると、CUDA/v11.2/binにあるcublas64_11.dllとサイズが違うことに気づいたので、v11.2にあるものをtorch/libにコピー。
ちゃんと動きましたん。

Firefoxが2.5GBくらい使っていたので、4GBちょいで動いた模様。なので8GBのGPUでも動かせそう。

Gradioでブラウザから使う

Gradio、素敵
https://gradio.app/

def generate(input):
    ...
    return text

のような関数を作って

llm = gr.Interface(
    fn = generate,
    inputs = gr.Textbox(lines=3, placeholder="質問を入力してください"),
    outputs = gr.Textbox(lines=3),
)
llm.launch()

という感じで関数への入出力に対応するUIを指定してあげるだけでこんなUIができる。

CPUだと15秒くらいで答えが返ってくる。 i7-7820Xなので、もっと新しいCPUであれば10秒切るんじゃなかろうか。

CPUでLLMを動かすというとllama.cppがありますが、やはりPythonのエコシステムが使えるのは いいですね。

GPUだと7秒くらい。A4000は結構遅いので、RTX40とかRTX30とか普通のゲーム用GPUであればもっと速そう。

import ctranslate2
import transformers
import gradio as gr

model_name = "rinna/japanese-gpt-neox-3.6b-instruction-ppo"
ct2_model = "rinna-ppo-ct"

generator = ctranslate2.Generator(ct2_model, device="cuda")

tokenizer = transformers.AutoTokenizer.from_pretrained(model_name, use_fast=False)

def generate(input):
    prompt = "ユーザー :" + input + "<NL>システム :"
    tokens = tokenizer.convert_ids_to_tokens(tokenizer.encode(prompt, add_special_tokens=False))
    results = generator.generate_batch(
        [tokens],
        max_length=256,
        sampling_topk=20,
        sampling_temperature=0.8,
        include_prompt_in_result=False,
    )
    text = tokenizer.decode(results[0].sequences_ids[0])
    return text

llm = gr.Interface(
    fn = generate,
    inputs = gr.Textbox(lines=3, placeholder="質問を入力してください"),
    outputs = gr.Textbox(lines=3),
)
llm.launch()

*1:2024時点では対応してるけど導入はめんどい

Javaがパブリックスタティックヴォイドメインの呪文から解放される

みなさん、今日もパブリックスタティックヴォイドメインしてますか?
ジャバと言えばパブリックスタティックヴォイドメインですよね。
最近はIDEmain[tab]と入力すれば補完してくれるとはいえ、コードを読むときには目に入ってきたりしますね。
そんなパブリックスタティックヴォイドメインの呪文から解放される日が近づいています。

TL; DR

9月リリースのJava 21の試用機能として

void main() {
  System.out.println("Hello");
}

をhello.javaで保存したら、java hello.javaで実行できるようになります。
(Java 21では--enable-preview --source 21が必要)

Javaではプログラムを開始するときにpublicclassなどにStringの配列を受け取るpublicstaticで戻り値を返さないmainという名前のメソッドを定義する必要があります。

public class Hello {
  public static void main(String[] args) {
    System.out.println("Hello Java!");
  }
}

最低限必要なのは この部分だけなのに、いろいろついています。

println("Hello Java!");

これが問題なのは、Javaプログラミングの開始時に教えないといけないことが多すぎるということです。クラス、アクセス指定、static、メソッド定義、引数、String、配列、クラスメンバー、インスタンスメンバー、メソッド呼び出しを知っておく必要があります。
この中で、メソッド呼び出しは機能実現に必要であるし、メソッド定義も結局早い時期に必要になるのであってもいいと思います。クラスも、まあそういう方針として許容範囲。
しかし、アクセス指定は複数のクラスを定義しないと説明ができません。そしてstaticはインスタンスを説明しないといけないし、staticなしのメンバーとのやりとりの説明がかなり大変です。配列も割と面倒。

ということで、9月リリース予定のJava 21から試用機能として次のように書けます。

void main() {
  System.out.println("Hello Java");
}

JEP 445としてまとめられています。
JEP 445: Unnamed Classes and Instance Main Methods (Preview)

JEP 445は先週のBuild 26で取り込まれて使えるようになっています。
https://jdk.java.net/21/

これを実行するには、次のように--enable-preview--sourceの指定が必要です。

> java --source 21 --enable-preview hello.java
ノート: hello.javaはJava SE 21のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Hello Java

一旦コンパイルする場合は、javaコマンドには--sourceは不要です。ソースファイル名と同じ名前のclassファイルができます。

>javac --enable-preview --source 21 hello.java
ノート: hello.javaはJava SE 21のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。

> java --enable-preview hello
Hello Java

けどまあ、コマンドラインで動かすときにわざわざコンパイルは しないですね。

クラスが書きたいならこれでもOKです。

class Hello {
  void main() {
    System.out.println("Hello Java");
  }
}

つまり、アクセス指定はprivate以外ならなんでもいい、mainString[]引数があってもなくてもいい、staticでもstaticじゃなくてもいい、クラスもあってもなくていい、ということになっています。
クラスがない場合は、実際にはObjectの匿名クラスということになります。

new Object() {
  void main() {
    System.out.println("Hello Java");
  }
}.main();

で、クラスとかstaticとか不要なのが、サンプルプログラムを書くのにとてもいいです。逆にいえば、いままでパブリックスタティックヴォイドメインはかなり邪魔になっていたということです。

高度に発展したジャヴァプログラマーにとってパブリックスタティックヴォイドメインは自動的に視界から消えていたわけですが、実際に消えたところを見ると、やっぱ全然違いますね。
これは、高度に発展したジャヴァプログラマーにとって匿名クラスは自動的にクロージャに見えていたのが、ラムダ式が導入されるとやっぱ邪魔だったというのと同じですね。

まあ、高度に発展したジャヴァプログラマーは誤魔化せるとしても、紙の雑誌の文字数は誤魔化せないのですね。
クラス定義でどんどん紙を圧迫してしまうので、完全な形のコードは気軽には示せず、クラスやmainを省略した形で載せることになっていました。その場合にpublic static void main(String[])を出してしまうと、言いたいことが隠れるので結局はコード部分だけ載せることになってました。

それが、たとえばStreamの説明でも次のコードでちゃんと動かせるプログラムとして提示できるわけです。

import java.util.stream.Stream;
void main() {
  Stream.of("apple", "banana")
    .map(String::toUpperCase)
    .forEach(s -> System.out.println(STR."これは\{s}です"));
}

クラスのimportとそれを使うコードが近くなるのでとてもいい。
あと、文字列に値を埋め込めるString Templateもいいですね。これもJava 21で試用機能として導入されます。
JEP 430: String Templates (Preview)

パブリックスタティックヴォイドメインがなくなると、ジャバに見えなくなります。
あとはSystem.outにジャバらしさが残りますが、これも恐らくそのうち消せます。
System.out.printlnをデフォルトのstatic importにしようという話をしています。というか、今回のJEP 445は次の「入口の舗装」という話の一部になっています。
Paving the on-ramp

去年の9月に話が出てきて今年の9月に実装されるというスピード感、いかにPythonのプレッシャー感じてるかがわかりますね。

ということで、6月24日発売のWEB+DB PRESS Vol.135でのサンプルコードはほとんどすべてvoid main()で囲んで書いています。ほかにもJava 21の機能を活かしながらサンプルを書いているので見てみてください。
コレハジャバデハナイとなります。
主題は、プログラムの処理をどう考えるかという話なので、ジャバじゃない人も読んでください。

※追記 19:35 JEP443にしてたのを、JEP445に修正しました。
あと、「プロジェクトに1回書けばいい」というコメントありますが、入門時には1ページに1回書いたりします。1時間の授業で3つ4つ書くこともありますね。それと、staticは入門時に非常に邪魔なのでなくなってありがたい。
C#がプレッシャ与えるほど追い上げているような気配はないし、C#対抗であればエントリポイント改善は有効ではないと思います。

※追記 20:58 シンタックスシュガーは覚えることが増えるという話ありますが、覚えるタイミングの問題です。入門したてで何もわからんときにいきなり覚えることが多すぎて、「見なかったことにして」と言わざるをえないのは教えるうえで結構な障害になります。あとstaticと非staticの使い分けがかなりやっかいです。
ある程度プログラミングを理解してから教えることができるようになるのは、とてもやりやすいです。

空の配列に対するmaxは何を返すか

ちょっと前に「配列中のすべての要素が条件を満たすかどうか判別する関数で、空の配列はTrueを返すべきかFalseを返すべきか」のような話が話題になってました。
まあこれは「Trueを返す」が答えなわけですが、では「配列中の最大値を返す関数で空の配列の場合は何を返すか」が気になりました。

「配列中のすべての要素が条件を満たすかどうか判別する関数」について言えば、簡単に言えばこんな感じ。
まず、配列のすべての要素が偶数であるかどうか判別する関数を考えます。

void main() {
  int[] data = { 23, 44, 12, 98, 5 };
  System.out.println(allEven(data));
}
boolean allEven(int[] data) {
  for (int n : data) {
    if (n % 2 != 0) return false;
  }
  return true;
}

これを配列の要素が0のときにfalseを返そうとすると、わざわざそのための条件を入れる必要があります。
ということで、trueを返すのが自然。

ただ、それだけだと「単に実装の都合なのに、Trueであるべきっていうのは納得いかん!」となる気がします。 実際には、「すべての要素が条件を満たすかどうか」という処理の性質としてTrueを返すのが自然なので、それについて説明してみます。

その前に、数値を全部合計する処理を考えてみましょう。

void main() {
  int[] data = { 23, 44, 12, 98, 5 };
  var result = 0;
  for (int n : data) {
    result = result + n;
  }
  System.out.println(result);
}

ちなみに今回のコードは、Java 21の試用機能として、そのまま実行できるようになる予定。
まだ試用機能なので--enable-previewが必要ですが、来年のJava 23くらいには正式機能になるはず。

で、他に、数値を倍にしたリストを得る処理を考えてみます。

import java.util.*;
import java.stream.*;
void main() {
  int[] data = { 23, 44, 12, 98, 5 };
  var result = new ArrayList<Integer>();
  for (int n : data) {
    result = Stream.concat(result.stream(), Stream.of(n * 2)).toList();
  }
  System.out.println(result);
}

この2つの処理は、同じようなパターンになってますね。
ということで、allEvenも同じようなパターンで書いてみましょう。

void main() {
  int[] data = { 23, 44, 12, 98, 5 };
  var result = true;
  for (int n : data) {
    result = result & (n % 2 == 0);
  }
  System.out.println(result);
}

これらの処理で、違っている部分をまとめると次のようになります。

処理 初期値 計算処理
sum 0 +
toList 空リスト concat
allEven true &

ここでどういう値が初期値になっているか見てみると、計算処理を行っても相手に影響を与えない値になっています。
つまり、a+0も0+aもaになり、aに空リストを連結しても 空リストにaを連結してもaになり、そして、a & TrueもTrue & aもaになります。 こういった、演算しても相手に影響を与えないような値を、その演算の単位元といいます。
ということで、「すべての要素が条件を満たすかどうかという処理の演算は &演算なので、空配列のときの値は &演算の単位元であるTrueを返すのが適切」となります。
ちなみに「場合による」と言ってる人が叩かれたりしてましたが、場合によるときには要件が「すべての要素が条件を満たすかどうか」ではなく、例えば「カートが出荷可能か(カートの中のすべての商品が出荷可能であり、しかしカートが空であれば出荷可能ではない」のような別のビジネス要件になるはず、という感じですね。

さて、ここで本題の「空の配列に対するmaxは何を返すか」です。
これを先ほどと同じパターンで書いてみると次のようになります。

void main() {
  int[] data = { 23, 44, 12, 98, 5 };
  var result = Integer.MIN_VALUE;
  for (int n : data) {
    result = Math.max(result, n);
  }
  System.out.println(result);
}

となると、maxの単位元は-∞ってことになるんでは、となります。 というところで、Javaのライブラリを見てみます。CollectionsmaxNoSuchElementException、StreamのmaxOptionalを返します。

jshell> Collections.max(List.<Integer>of())
|  例外java.util.NoSuchElementException
|        at ImmutableCollections$ListItr.next (ImmutableCollections.java:375)
|        at Collections.max (Collections.java:674)
|        at (#2:1)

jshell> IntStream.of().max()
$3 ==> OptionalInt.empty

これは、「オブジェクトを考えると単位元になる最小値が一意に決まるとは限らない」ということで、数値以外を考慮するならemptyや例外になる、となっています。JavaScriptのmaxは-Infinityを返すらしい。
つまり、これは「場合による」が正しいということに。

こういった話は数学だ、と言われてもあまり学校で習った数学ではなさそう。 じゃあ何かとなるのだけど、これは「離散数学」という分野を勉強するとよさそう。離散数学の中でも、代数型とか群・環・体とかいうやつ。
で、「離散数学」で勉強しようとすると難しそうな本が多かったりするので、この本くらいがおすすめ。

ということに近い話をWEB+DB PRESS Vol.135に書いていたところだったので、少し詳しく書いてみました。
「ループと状態遷移、ついでにJava 21」として、プログラムの処理中の状態遷移についてまとめています。コードサンプルにJava 21を使っているので、非常にコンパクトに完動するコードサンプルを多く載せれていて、これはいいなとなっています。興味ある人は見てみてください。