白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

TSKaigi2024レポ

こんばんは。

TSKaigi2024にオンライン参加したのでちょっとだけ感想を。
カンファレンス的なものの参加ってめちゃくちゃ久しぶりなのですが、イベント参加したら何か書けと偉い人も言ってる気がするので簡単な感想と聞きながら書いてたメモで。
変に矢印とか入れちゃったんで箇条書きになってないとか、話の内容と個人の感想が混じってたりするんですがそのあたりは許してくださいということで。(だから最初からmarkdown的なもので書けと…)
LTもいくつか聞いたんですが、そこは省略で。

Keynote: What's New in TypeScript

MicrosoftのTypeScript Principal Product Managerの方のセッション。
オンラインの配信トラブルで15分ほど遅れで開始。

同時翻訳があるのはうれしいんですが、字幕とスライドのどっちを見たらいいんだ状態にはちょっとなりがち。
そもそもTSにまだそんなに詳しくないので、全体的に「へー」と思いつつもあんまり苦労した思い出がないので、やっぱり普段使う言語で便利機能が追加されるときほどの感動がないのはちょっと寂しいですね。

・TSのニーズ高いな(Rust強いな)
・Inferの型絞り込み便利そう
・filterのラムダがtype guard的に動くのか(というかもともとそうじゃないのか…)
コンパイル速度とかは業務でちゃんとしたでかいサイズ扱わないと目が向かないところだなー
・型を型だけでコンパイルするって面白い
・書いてる側としては戻り値も型推論されちゃうから省略しがちだけど、使う側としては確かに書いておいてほしいかも

フロントエンドもバックエンドもインフラも… 全てをTypeScriptで統一したらこうなった!

JSは動かしてみないとわからない綱渡りっていうのは確かにそうだなと。
インフラも込みでTS率95%超えとかできるんですね…。

・レイヤー参考になるー!(TS関係ない)
GitHub Codespacesで環境構築いいな
・型はzodでスキーマ定義→inferで型に変換(スキーマと型ってごっちゃにしがちかもしれない)
・ZodiosはコードがAPI定義書っぽくていいな
・統一すると全員フルスタックエンジニアになって、分業によるタスクの偏りは減る
・知識の共有が開発言語の違いによって阻害されない

TypeScript 関数型バックエンド開発のリアル

伊藤さんのTSで関数型DDDの話は「Domain Modeling Made Functional」を読んでいい感じに取り入れられないかと思っていた時にいろいろ参考にさせてもらっていました。
やっぱり型をちゃんとするとコンパイル時点でおかしいことに気付けるし、ユニットテストの分量も減らせるのがいい。

ドメインレイヤー以外はオブジェクト指向とあんまり変わらない
・Union型を使ってとにかく型をよく使う(オブジェクトだけじゃなくて関数も)
・オブジェクトの変更は関数適用による状態遷移でイミュータブル
→やりたいんだけどC#だとUnion型、分割代入、スプレット構文とかがなくてTSがうらやましく感じる
・命令的よりも関数的な方が明示的に変化が現れる
→型を厳密に書けば書くほどコンパイラがちゃんとチェックしてくれるのはいいよね
・データ取得とかデータ保存とかは普通に書いていい(あくまでドメイン層だけ)
→IOでサンドイッチ(=オニオンアーキテクチャ
ドメインレイヤーをIOと分離することで、ドメインレイヤーを関数型スタイルにする余地が生まれる
ユニットテストでモック差し込むときとかもなるべく薄くはぎ取って差し込みたいよね
・Resultパズルがつらい
→Result.combineでmapする、fromThrowable/fromPromiseとか使う
→一直線のWFならいいんだけど、この辺り単純なRailwayの考え方だけだと足りないんだよなと最近悩んでいるところ

複雑なビジネスルールに挑む:正確性と効率性を両立するfp-tsのチーム活用術

エラーをいっぺんに出してっていう話は確かによくあるなと。
Visitorパターンとか使って貯めこんでもいいけど、合成できちゃうならResultでつなげていくのは確かに楽でいいですよね。

・セルの検証→行の検証→表の検証ってステップが複数ある
・関数型でエラーの合成してEiher型(Result型)で返す
・例外だと複数のエラーを同時に伝搬しづらい
→これってRailwayのエラー側に流すのと共存できるのかしら
・エラーの合成自分ではやりたくない
→fp-tsを使うとエラーの合成がしやすい(pipeとかmapとか使って合成できる)
・公称型を利用して検査の関門を一つに絞る
・やっぱ関数型の学習コストは結構高い
→スコープを絞るとかペアプロ・モブプロするとか
・パフォーマンス的にいいんだっけは要検証

Step by Stepで学ぶ、ADT(代数的データ型)、モナドからEffect-TSまで

型定義を見ると挙動がわかるから宣言的って話はそうだよなーと思いつつ、型パズルをするのをわかりやすいとみなせるかは慣れにもよりそうだなと。
非同期周りはどうしても複雑になりがちなので、Effectで吸収してくれるのはありがたい。

・validateってtrue/falseのどちらが成功か人によって違う
・どんなエラーが投げられるかわからないし読みづらい
・union型でどんなエラーが返ってくるか型で分かる
・Rightはvoidでもいいよ(Rightって正しいだからエラーはLeftへ)
・TSの言語仕様としてpipeはいるかも?
→知らなかったので調べよう
・if文がなくなってデータフローとして表現できるけど型パズルにはなる
・非同期のためのTaskとTaskEither
→実態は()=>Promise
→実行されるとTaskEitherからPromiseEitherになる
・Effect
→PromiseとTask区別せずに全部Effectで揃うので楽
・処理が小さくなるのでメンテナンス性が上がるし、ユニットテストが書きやすい
→うんうん

サービス開発におけるVue3とTypeScriptの親和性について

Reactは多少触ったことがあるんですが、VueはノータッチだったのでこれがVueならではの話なのかの判断が難しかったですね。
TSで型ついてうれしーとか雑に思ってるけど、やっぱりそこに至るまでにはいろいろな苦労があるんだなと。

・親和性=型付け×論理構成
・Props Drilling/Event Emitting
・Vue2は型の恩恵がいまいちだった
→結構対応が大変だった
コンポーネント自体は対応できてもVSCodeとかのツールへの対応が大変だった(vuejs/lauguage-tools)
・フロントエンドってコンポーネントに分けるところまではできても、コンポーネントの中散らかしがちだよなぁ…
・PiniaをつかってProvide/Injectを使うとスコープを制限できる
→なんでもグローバル…うっ頭が…

TypeScriptとGraphQLで実現する型安全なAPI実装

APIからの型付けは祈りでしかない。確かに。
GraphQL自体をつかっていないので、なるほどこうやって作っていくんだなという感じで面白かったです。

・型をつけるとは
APIの仕様をスキーマで定義、スキーマから型を生成(自動で)、生成された型で実装
APIに型をつける技術としてはOepnAPI、gRPC、GraphQLあたり
・GraphQL Codegen

クライアント側

スキーマから生成した型ではなくてクエリから生成した型を使う
スキーマから生成すると参照側で省略した値が消えてエラーになっちゃう
・すべての子が必要とするデータを知ってる必要がある
→フラグメントコロケーション
→クエリで指定するフィールドをフラグメントとして親に渡してマージする(コンポーネントごとに定義)
・定義してないところでも使えちゃうやんけ(ダックタイピング的な話ね)
→フラグメントマスキングでエラーにしてくれるけど、型が複雑になるデメリットはある

サーバ側

・サーバ側はクエリじゃなくてスキーマ側からつくるでいい
・いらないもの取るってどうするの?
→Field Resolver(必要なものが入ってるときだけ遅延実行される)
→これ割と前から疑問だったのでなるほどなと思うなどする
・mappersでresolverが使う型を指定できる

Prettierの未来を考える

Prettier使っていますが、作る人としてはこんなこと考えてるんだなーと。
個人的にはnode.jsって結構いろんなもの組み合わせる必要があって難しいなと思ってたんですが、詳しい人も難しいならそりゃ難しいわと思いました。

・コーディングスタイルって個人の価値観によっていろいろある
→争いをなくす
・最近の言語だと言語側がこういうの提供されてたり(GoとかRust)
・単品で使うと便利なんだけど、ESLintとかと一緒に使うと…
→コードフォーマッターであってリンターじゃない
・しかもTSだともっといっぱい入れないといけない
・DenoってTS直で使える(node.jsじゃないよ)
・node.jsつかうならBiomeがアツイ
→DenoもBiomeも使ったことないので試してみたい
・パーサーを自分で持ってないのは強いなー

というわけで

イベントスタッフのみなさま、発表者のみなさま、スポンサーのみなさま、参加者のみなさま。
おつかれさまでした。

AIのビジネスアイデアを考えてもらおうぜAIに

こんばんは。
衣替えをしたんですが、そういえばスプリングコートとか一回も着ていないことに気付きました。

日々生成AIの新しいモデルが公開されて群雄割拠の時代という感じですが、なんかすごいことができそうな気がする割にアイデアが思いつきません。
じゃあ自分でアイデア考えてくれよということで試しに作ってみました。

今回作ったもののコードはこちら。
github.com

とりあえずGPT-4に聞いてみますか

あなた
生成AIをテーマにしたビジネスアイデアを10個挙げてください

ChatGPT
AIをテーマにしたビジネスアイデアを10個挙げますね:

1. AIパーソナルアシスタントサービス - ユーザーの日常生活やビジネス活動をサポートするためのカスタマイズ可能なAIアシスタントを提供します。スケジュール管理、メールの自動返信、健康管理など多岐にわたるサービスを提供。
2. AI駆動のリモート監視システム - 製造業や物流業界向けにリアルタイムでの機械やシステムの監視を行うAIソリューション。故障予知や最適化提案を自動で行い、運用コストの削減を実現。
3. AIベースのエデュケーションプラットフォーム - 個々の学習者のニーズに合わせたカスタマイズ可能な教育コンテンツを提供するプラットフォーム。AIが学習進度や理解度を分析し、最適な学習プランを提案。
4. AIを活用した健康診断アプリ - ユーザーの日常の健康データを分析し、早期の健康問題を警告するアプリ。病歴やライフスタイルに基づいてパーソナライズされた健康改善提案を提供。
5. AI動画編集ツール - AIが自動でビデオの編集を行うツール。映像のクオリティを自動で向上させるフィルターや、効果的なカット編集を提案。
6. AI音声認識顧客サービス - 電話やチャットでの顧客サービスをAIが自動対応。顧客の要望に基づき、迅速かつ正確な対応を行い、顧客満足度を向上させる。
7. AIマーケット分析ツール - 市場データをリアルタイムで分析し、ビジネスの意思決定を支援するツール。トレンドの予測や競合分析が簡単に行える。
8. AIスマートホームソリューション - 家の中のあらゆるデバイスをAIで制御するシステム。エネルギーの最適化、安全性の向上、生活の便利さを提供。
9. AIによる法律アドバイザリーサービス - AIが法的ドキュメントを分析し、法律問題に対するアドバイスを提供するサービス。小規模企業や個人でも手頃な価格で法律サービスを受けられる。
10. AIベースのフリーランスマッチングプラットフォーム - フリーランサーとプロジェクトのマッチングをAIがサポート。スキルとプロジェクト要件の最適な組み合わせを提案し、効率的な人材配置を実現。

適当なプロンプトだけどなんか割とそれっぽいですね。
でも何回か繰り返してるとわかるのですが、割と似たようなアイデアばかりになってきます。

2段階作戦

毎日動かして貯めたいので、バリエーションが欲しいです。
なので、

  1. 適当にキーワードを挙げてもらって貯める
  2. 貯めたキーワードの中からランダムで抽出して、それらを組み合わせてアイデアを出す

という2段階のフェーズを踏む作戦にしました。

キーワードを貯める

とりあえずこんな感じにしてみました。LangChainのJS版を使っています。
CommaSeparatedListOutputParserでカンマ区切りを配列に変換できますが、たまーになんか変な動きをします。
「日本語でカンマ区切りで新しいサービスのアイデアを1単語のキーワードを出力してね。数字はいらないよ。」って感じですかね。調整の余地はありそうです。

const chatTemplate = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("You are a creative assistant. Use Japanese as the language. Your response should be a list of comma separated values, eg: `foo, bar, baz`."),
    HumanMessagePromptTemplate.fromTemplate(`Please output {size} one-word keywords in Japanese to generate ideas for new services. Should not output number! only keyword.`)
]);

const chain = new LLMChain({
    llm: this.model,
    prompt: chatTemplate,
    outputParser: new CommaSeparatedListOutputParser()
});

const result = await chain.invoke({ size: size });
return result.text as string[];

しばらく貯めているとこんな感じになります。何かほかの用途にも使えそうな気もします。
モデルがあれこれあるのはいろいろ試してみてるからです。こういうときにLangChainは便利ですね。
(スキンケアゲーム、スキンケアゲームゲーム…なにそれ)


イデアを作ってもらう

ランダムにキーワードを抜き出すあたりはSQLでやっています。そのあたりはGitHubを見てもらうということで。

で、プロンプトとかOutputParserとかはこっちの方がちょっと複雑ですね。
「あなたはクリエイティブなアシスタントだよ(洗脳)。与えたランダムなキーワードから0~2つをヒントに"生成AI"というコンテキストで現実には存在しないアイデアを考えてね。」みたいな感じです。これも調整の余地はありそうです。
今回はオブジェクトにそのままマッピングしたいのでStructuredOutputParserを使って構造を与えています。

const keyword: string = keywords.join(",");

const chatTemplate = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("You are a creative assistant; output results in JSON. Use Japanese as the language."),
    HumanMessagePromptTemplate.fromTemplate(`In the context of generative AI, create a concept that does not exist in reality, name it and output a brief description of it.
Below are some random keywords, and I hope you will use 0 to 2 of them as hints if necessary (you don't necessarily have to use them).
{keyword}

Please submit {cnt} ideas.
{formatInstructions}`)
]);

const outputParser = new StructuredOutputParser(
    z.object({
        ideas: z.array(z.object({
            name_en: z.string().describe("English name of the concept"),
            name_jp: z.string().describe("Japanese name of the concept"),
            description_jp: z.string().describe("Description of the concept about 200 token"),
            tags_jp: z.array(z.string()).describe("Tags for this concept"),
        }))
    })
);
const formatInstructions = outputParser.getFormatInstructions();

const chain = new LLMChain({
    llm: this.model,
    prompt: chatTemplate,
    outputParser: outputParser
});

const result = await chain.invoke({ keyword: keyword, cnt: cnt, formatInstructions: formatInstructions });
return result.text.ideas as LlmResult[];

こんな感じです。いちいち名前がかっこいい。


で、いいアイデアはできるの?

正直微妙…。
感覚的にはGeminiはイマイチ、GPT-3.5は平凡で簡潔、Claude3 HaikuとCommandR+はそこそこの精度でそこそこの文章量。GPT-4はGPT-3.5よりいい感じではあるけどちょっとAPI利用料が…。
そういえばLlama3は試してないですね。

いっぱい回してるとやっぱり傾向はあるみたいで、やたら夢をコントロールしたがったり、時間を超えようとしたり、宇宙に行ったり、新しい芸術を作ったりしたがります。
この辺りは現実世界には存在しない概念みたいなプロンプトが効きすぎているのかもしれないですね。

こんな感じの画面も作ったけど、これはだいぶ適当につくってしまったので、気が向いたらそのうち公開するということでね…。

みんなでガチャしましょう。アイデア料は収益の1%でいいですよ。(嘘です)

デフォルト引数があってもシグネチャが変わるから互換性はないよ

こんばんは。
暑いです…四月なのに…。

さて、すごく久しぶりにC#のネタです。

ライブラリってバージョンアップするよね

C#のパッケージマネージャといえばNuGetですが、1.0.0のようにx.y.z形式でバージョニングします。
後方互換のないバージョンアップはメジャーバージョンのxを、後方互換性のある機能追加はマイナーバージョンのyを、後方互換性のあるバグ修正などはパッチバージョンzを上げます。

こういうバージョニングを
セマンティックバージョニング - Wikipedia
といいます。

NuGetサーバ用意するの面倒なので

こんな感じの構成を作ります。

OldLibraryとNewLibraryはアセンブリ名をLibにして、どちらもLib.dllができるようにします。
SampleはLib.dllをプロジェクト参照ではなくアセンブリとして参照している状態です。
これでOldLibraryのdll参照時が古いバージョン(1.0.0)、NewLibrary参照時が新しいバージョン(1.1.0)ということにしましょう。

で、さらにこうします。

もう一つライブラリが増えました。
このとき、OtherLibraryはOldLibraryのLib.dllを、SampleはNewLibraryのLib.dllを参照させます。
これは例えばOtherLibraryの依存関係がLib (>= 1.0.0)とかにLibのバージョンを1.1.0にバージョンアップしたような状態です。


メジャーバージョンあげるのって面倒だよね

さて、準備ができたのでOldLibraryとNewLibraryをバージョンアップ前後の状態にしていきましょう。
すごく簡単なクラスを作って、メソッド2に任意の引数を受けられるように改修したいとします。

// ver. 1.0.0
public class Library
{
    public static string Method1()
    {
        return "Hello World";
    }

    public static string Method2()
    {
        return "Hello World";
    }
}

こんな感じ。利用側でメソッドシグネチャが変わると面倒なのでデフォルト引数をつけてあげることにしました。
これで互換性があるのでマイナーバージョンアップで済みそうです。

// ver. 1.1.0
public class Library
{
    public static string Method1()
    {
        return "Hello World";
    }

    public static string Method2(string target = "World")
    {
        return $"Hello {target}";
    }
}

せっかくなのでユニットテストも書きましょう。ばっちりOKです。

[TestMethod]
public void TestMethod1()
{
    Assert.AreEqual("Hello World", Library.Method1());
}

[TestMethod]
public void TestMethod2()
{
    Assert.AreEqual("Hello World", Library.Method2());
}

[TestMethod]
public void TestMethod2_引数あり()
{
    Assert.AreEqual("Hello Universe", Library.Method2("Universe"));
}

なんかエラーになるぞ

次にOtherLibraryの定義をこんな感じにします。

public class OtherLibrary
{
    public static string Method1()
    {
        return Library.Method1();
    }

    public static string Method2()
    {
        return Library.Method2();
    }
}

Sampleの定義をこんな感じにします。

public class Class
{
    public static string Method1()
    {
        return Library.Method1();
    }

    public static string Method2()
    {
        return Library.Method2();
    }

    public static string Method3()
    {
        return OtherLibrary.Method1();
    }

    public static string Method4()
    {
        return OtherLibrary.Method2();
    }
}

で、テストを書きます。

[TestMethod]
public void TestMethod4()
{
    Assert.AreEqual("Hello World", Class.Method1());
}

// こっちも結果的に同じメソッドを同じように呼んでいるのに
[TestMethod]
public void TestMethod5()
{
    Assert.AreEqual("Hello World", Class.Method2());
}

[TestMethod]
public void TestMethod6()
{
    Assert.AreEqual("Hello World", Class.Method3());
}

// なぜかこっちだけエラーになる
[TestMethod]
public void TestMethod7()
{
    Assert.AreEqual("Hello World", Class.Method4());
}

何故だかOtherLibrary経由のMethod2の呼び出しだけエラーになってしまいました。
エラーの原因を見てみます。どうやらメソッドが見つからないといわれているようです。

System.MissingMethodException: Method not found: 'System.String Lib.Library.Method2()'.

はい。というわけでデフォルト引数はシグネチャが変わります。なので互換性がありません。
直接使う分にはあたかも互換性があるように見えるのが厄介なところですね。

バージョン設定は慎重に

メジャーバージョンアップとマイナーバージョンアップを間違えると怖いよねって話でした。
そしてメジャーバージョンアップは慎重にね。

それでは。