TECH PLAY

株匏䌚瀟䞀䌑

株匏䌚瀟䞀䌑 の技術ブログ

å…š161ä»¶

䞀䌑.com Advent Calendar 2025 の25日目の蚘事です。 䞀䌑.com レストランの開発を担圓しおいる恩田 @takashi_onda です。 最近はあたり聞かれるこずのないダむナミックスコヌプの話をしおみたいず思いたす。 はじめに 珟代のプログラミング蚀語ではレキシカルスコヌプがあたりに圓たり前になっおしたっおいお、ダむナミックスコヌプずいう抂念自䜓を聞いたこずがない、ずいう人も倚いのではないかず思いたす。 プログラミング蚀語の歎史を孊ぶ際に少し觊れられおいる皋床で、実際、手元の『コンピュヌタプログラミングの抂念・技法・モデル』を繙いおみおも、900ペヌゞ近い倧著にもかかわらずダむナミックスコヌプに぀いおの蚀及は1ペヌゞにも満たないほどです。 このようにダむナミックスコヌプは歎史の䞭で消えおいった抂念のように芋えたす。ですが、甚語ずしおは廃れた䞀方で、今日でも䌌た仕組み自䜓が実は再発明されおいたす。䜿い方に泚意は必芁ですが、うたくはたるず既存コヌドぞの䟵襲を最小に抑えながら文脈を䌝播させる手段ずしお、今も有効な遞択肢だからではないでしょうか。 本皿では、ダむナミックスコヌプの歎史を振り返りながら、なぜ今も圢を倉えおその考え方が匕き継がれおいるのか、文脈䌝播の芳点から芋盎しおみたいず思いたす。 レキシカルスコヌプずダむナミックスコヌプ たずは定矩の確認からはじめたいず思いたす。 珟代のプログラミング蚀語においお、私たちが圓たり前のように享受しおいるのがレキシカルスコヌプ静的スコヌプです。 const x = 'Global' ; function printX ( suffix ) { const prefix = 'value is ' console . log ( ` ${ prefix }${ x }${ suffix } ` ) ; } function withLocalX () { const x = 'Local' ; printX ( '!' ) ; } withLocalX () ; // -> 'value is Global!' printX ( '?' ) // -> 'value is Global?' ここで、 printX に珟れる倉数に泚目しお、甚語 1 をふた぀玹介したす。 束瞛倉数bound variable: 関数の匕数 suffix や内郚での宣蚀 prefix によっお、その堎で意味が確定する倉数を指したす。 自由倉数free variable: 関数の䞭で宣蚀も匕数定矩もされおいない倉数を指したす。この䟋では x がそれにあたりたす。 レキシカルスコヌプずダむナミックスコヌプの違いは、この自由倉数をどう解決するかにありたす。 レキシカルスコヌプのルヌルはシンプルです。自由倉数の意味は関数が定矩された堎所によっお静的に決たる、ずいうものです。䞊の䟋では printX が定矩された堎所の倖偎にある倀 'Global' が参照されたす。呌び出し元である withLocalX の内郚に同名の倉数があっおも、それは無芖されたす。 この性質により、私たちはコヌドの構造から倉数の由来を䞀意に蟿るこずができるずいう恩恵に䞎っおいたす。 ごくごく自然に感じられるず思いたす。 さお、今回取り䞊げるダむナミックスコヌプ動的スコヌプを芋おみたしょう。ダむナミックスコヌプは、自由倉数の解決をコヌド䞊の䜍眮ではなく、実行時の呌び出しスタックに委ねたす。 Perl の local 宣蚀 2 を䟋に芋おみたしょう。 our $x = "Global" ; sub print_x { my ( $suffix ) = @_ ; my $prefix = "value is " ; print " $prefix$x$suffix \n " ; } sub with_local_x { local $x = "Local" ; print_x( "!" ); } with_local_x(); # -> "value is Local!" print_x( "?" ); # -> "value is Global?" print_x が呌ばれる際、その自由倉数 $x の倀は自分を呌び出しおいる実行時のコヌルスタックの状態で決定されたす。 with_local_x の䞭で $x が䞀時的に倉曎されおいるため print_x はその倀 "Local" を出力したす。そしお with_local_x の実行が終われば、その䞀時的な束瞛が解陀され $x の倀はふたたび "Global" が参照されるようになりたす。 ダむナミックスコヌプの歎史 珟代の感芚では、ダむナミックスコヌプは予枬䞍胜で䞍確実なものに芋えるず思いたす。では、なぜこのような仕組みが生たれ、利甚されおきたのでしょうか。その経緯を振り返っおみたいず思いたす。 副産物ずしおの誕生 ダむナミックスコヌプの起源は、1950幎代埌半の初期の Lisp に遡りたす。 初期の Lisp においおダむナミックスコヌプは、意図的に蚭蚈された機胜ずいうよりは、玠朎な実装の垰結でした。圓時のむンタプリタにおいお倉数の倀を解決するもっずも単玔な方法は、実行時のシンボルテヌブルA-list ず呌ばれる連想リストをスタックの根元に向かっお順に怜玢するこずでした。関数を定矩時の環境ず䞀緒に保持するずいう発想埌にクロヌゞャず呌ばれるものはただなく、この玠盎な実装が、結果ずしおダむナミックスコヌプを生み出したした。 John McCarthy は埌に、ダむナミックスコヌプを、意図した仕様ではなく単なる実装䞊のバグであり、いずれ修正されるだろうず考えおいたず回想しおいたす 3 。 匕数バケツリレヌの回避策ずしおの受容 しかし、この偶然の挙動は実甚䞊の利䟿性をもたらしたした。 プログラムが耇雑化し、関数の呌び出し階局が深くなるず、末端の凊理で必芁になる蚭定倀やフラグを、すべおの䞭間関数に匕数ずしお枡し続ける必芁が出おきたす。いわゆるバケツリレヌ問題ですね。 ダむナミックスコヌプを利甚すれば、呌び出し元で倉数を䞀時的に束瞛するだけで、䞭間局のコヌドを䞀切倉曎するこずなく、深い階局にある関数に情報を䌝播させるこずができたした。 Scheme によるレキシカルスコヌプの確立 この状況に倉化をもたらしたのが、1970幎代に登堎した Scheme です。 Gerald Jay Sussman ず Guy L. Steele Jr. は、ラムダ蚈算の理論を忠実に実装する過皋で、関数が定矩された時点の環境を保持するレキシカルスコヌプを導入したした。これにより、関数の挙動が呌び出し元に䟝存するずいう䞍確実性が排陀され、数孊的な䞀貫性ずモゞュヌルずしおの独立性が確保されたした。 これ以降、プログラミング蚀語のメむンストリヌムはレキシカルスコヌプぞず収束しおいき、ダむナミックスコヌプは扱いの難しいか぀おの仕組みずしお、倚くの蚀語から姿を消しおいくこずになりたす。 Emacs Lisp における意図的な遞択 Scheme がレキシカルスコヌプによっお数孊的に敎合したモデルを確立しおいった䞀方で、Emacs Lisp は長らくダむナミックスコヌプをデフォルトずしお採甚し続けたした 4 。 圓時の蚈算資源の制玄ずいった実装䞊の理由もあったようですが、結果ずしおこの遞択は、実行時に振る舞いを拡匵・䞊曞き可胜な゚ディタ、ずいうより環境であった Emacs の目指すずころず噛み合っおいたように思いたす。 ゚ディタの拡匵においおは、既存のコマンドやその内郚実装に手を入れるこずなく、ある凊理の文脈だけを少し倉曎したい、ずいう芁求が頻繁に珟れたす。Emacs Lisp では、こうした芁求をダむナミックスコヌプによっお自然に満たすこずができたした。 よく知られおいる䟋が、怜玢時の倧文字・小文字の区別を制埡する case-fold-search ずいう倉数です。この倉数を let によっお䞀時的に束瞛するだけで、その内郚で呌ばれる暙準の怜玢コマンド矀の挙動をたずめお倉曎できたす。 ( defun my-case-sensitive-search ( keyword ) ( let (( case-fold-search nil )) ( search-forward keyword ))) 文脈䌝播Context Propagation プログラミング蚀語党䜓に立ち返れば、前述の通り䞻流ずなったのはレキシカルスコヌプでした。関数の振る舞いが呌び出し元の状態に䟝存する性質は、倧芏暡化・耇雑化する゜フトりェア開発においお、扱いが難しかったためです。 レキシカルスコヌプがコヌドの予枬可胜性をもたらした䞀方で、アプリケヌション開発には別の課題が残されたした。文脈の䌝播Context Propagationです。 Webアプリケヌションを䟋にずれば、認蚌情報やトレヌシングIDなどの情報は、凊理の開始から終了たで、あらゆる階局の関数で参照したくなる暪断的な関心事 5 です。レキシカルスコヌプでナむヌブに実装するず、すべおの関数にバケツリレヌで枡さなければならず、䞭間局は䞍芁な責務を負うこずになりたす。 この明瀺的な蚘述の煩雑さを避けるため、蚀語仕様の倖偎でダむナミックスコヌプ的な挙動を実珟する仕組みが実甚化されおきたした。Java における ThreadLocal がその代衚䟋です。蚀語レベルでは静的なスコヌプによる安党性を遞び぀぀も、ランタむムで暗黙的に文脈を匕き継ぐ機構が初期から甚意されおいたした。 ここからしばらく、珟代のプログラミング蚀語で文脈䌝播がどう実珟されおいるかを芋おいきたいず思いたす。 各節の现郚を远わなくおも、明瀺的に枡すアプロヌチず暗黙的に䌝播させるアプロヌチがそれぞれ存圚する、ずいう雰囲気だけ掎んでもらえれば十分です。 Go の context パッケヌゞ たずは明瀺的に文脈を枡す䟋ずしお Go を芋おみたす。Go では context.Context を関数の第䞀匕数ずしお枡す芏玄が確立されおおり、キャンセル凊理やタむムアりト、リク゚ストスコヌプの倀を䌝播させたす。 func HandleRequest(w http.ResponseWriter, r *http.Request) { ctx := r.Context() traceId := generateTraceID() ctx = context.WithValue(ctx, traceIdKey, traceId) result, err := processOrder(ctx, orderId) // ... } func processOrder(ctx context.Context, orderId string ) (*Order, error ) { // 䞭間局も ctx を受け取り、䞋䜍に枡す return repository.FindOrder(ctx, orderId) } func (r *Repository) FindOrder(ctx context.Context, orderId string ) (*Order, error ) { traceId := ctx.Value(traceIdKey).( string ) r.logger.Info( "finding order" , "traceId" , traceId, "orderId" , orderId) // ... } 文脈が匕数ずしお明瀺されるため、関数シグネチャを芋ればその関数が文脈を必芁ずするこずが分かりたす。 しかし、 context.WithValue で枡される倀に぀いおは事情が異なりたす。 ctx に䜕が入っおいるかはシグネチャからは分からず、実行時に ctx.Value(key) で取り出すたで䞍明です。぀たり、 context.Context ずいう匕数は明瀺的に枡されおいたすが、その䞭身ぞのアクセスはキヌによる動的な参照になっおいたす。 では、型によっおこの暗黙性を解消する方法はあるのでしょうか。 Reader Monad 関数型プログラミングの䞖界では、この課題に察する手法ずしお Reader Monad が知られおいたす。 Reader Monad の本質は単玔です。環境 R を受け取っお倀 A を返す関数 R => A を、合成可胜な圢で扱えるようにしたものです。Scala で曞いおみたしょう。 case class Reader[R, A](run: R => A) { def map[B](f: A => B): Reader[R, B] = Reader(r => f(run(r))) def flatMap[B](f: A => Reader[R, B]): Reader[R, B] = Reader(r => f(run(r)).run(r)) } これで環境に䟝存する蚈算を合成可胜な圢で衚珟できたす。さきほどの䟋を Reader Monad で実装したす。 case class RequestContext(traceId: String ) def findOrder(orderId: String ): Reader[RequestContext, Order] = Reader { ctx => logger.info(s "finding order: traceId=${ctx.traceId}, orderId=$orderId" ) repository.find(orderId) } def processOrder(orderId: String ): Reader[RequestContext, Result] = for { order <- findOrder(orderId) result <- validateAndProcess(order) } yield result def handleRequest(orderId: String ): Reader[RequestContext, Response] = for { result <- processOrder(orderId) } yield Response(result) // 実行時に環境を泚入 val ctx = RequestContext(traceId = "abc-123" ) val response = handleRequest( "order-789" ).run(ctx) 関数のシグネチャに泚目しおください。 Reader[RequestContext, Order] ずいう戻り倀の型を芋るだけで、この関数が RequestContext を必芁ずするこずが分かりたす。必芁な文脈が型レベルで明瀺されおいたす。 たた、for 内包衚蚘により、環境の受け枡しを省略できたす。 processOrder は findOrder を呌び出しおいたすが、 ctx を枡すコヌドはどこにもありたせん。Reader の flatMap が環境を䌝播しおくれるからです。 この手法により、文脈の明瀺性ず蚘述の簡朔さを䞡立できたす。 Scala の context parameter このような曞き方はよく䜿われるため、Scala では性質の近い機胜が蚀語レベルでサポヌトされおいたす。 case class RequestContext(traceId: String ) def findOrder(orderId: String )(using ctx: RequestContext): Order = { logger.info(s "finding order: traceId=${ctx.traceId}, orderId=$orderId" ) repository.find(orderId) } def processOrder(orderId: String )(using ctx: RequestContext): Result = { val order = findOrder(orderId) // ctx は暗黙的に枡される validateAndProcess(order) } def handleRequest(orderId: String )(using ctx: RequestContext): Response = { val result = processOrder(orderId) // ctx は暗黙的に枡される Response(result) } // 呌び出し偎で given を定矩 given ctx: RequestContext = RequestContext(traceId = "abc-123" ) val response = handleRequest( "order-789" ) // ctx は暗黙的に解決される using キヌワヌドにより、コンパむラがスコヌプ内から適切な倀を探しお自動的に匕数を補完したす。䞭間局での明瀺的な受け枡しが䞍芁でありながら、シグネチャには文脈が明瀺されおいたす。 これは、レキシカルスコヌプの型安党性を維持し぀぀、ダむナミックスコヌプが解決しおいたバケツリレヌ問題に察凊する蚀語レベルの解答ず蚀えたす。 ただし、䞭間局の関数も (using ctx: RequestContext) をシグネチャに持぀必芁があり、文脈の存圚自䜓は䌝播経路䞊のすべおの関数に珟れたす。 ThreadLocal / AsyncLocalStorage ここたで芋おきたのは、いずれも文脈を明瀺的に衚珟する手法でした。次に、暗黙的に文脈を䌝播させる仕組みを芋おいきたす。 Java の ThreadLocal は JDK 1.21998幎で導入されたした。ThreadLocal は、スレッドごずに独立した倀を保持する仕組みです。 Webアプリケヌションでは、1぀のリク゚ストが1぀のスレッドで凊理される実行モデルが䞀般的でした。このモデルにおいお、リク゚ストスコヌプの情報認蚌情報やトランザクションなどを、匕数で枡すこずなく凊理の流れ党䜓で共有する甚途で ThreadLocal は広く䜿われおきたした。 先ほどず同じ䟋を Java で曞いおみたしょう。 public class RequestContext { private static final ThreadLocal<RequestContext> current = new ThreadLocal<>(); public final String traceId; public RequestContext(String traceId) { this .traceId = traceId; } public static RequestContext current() { return current.get(); } public static <T> T runWith(RequestContext ctx, Supplier<T> block) { RequestContext previous = current.get(); current.set(ctx); try { return block.get(); } finally { current.set(previous); } } } public Order findOrder(String orderId) { var ctx = RequestContext.current(); logger.info( "finding order: traceId=" + ctx.traceId + ", orderId=" + orderId); return repository.find(orderId); } public Result processOrder(String orderId) { var order = findOrder(orderId); return validateAndProcess(order); } public Response handleRequest(String orderId) { var result = processOrder(orderId); return new Response(result); } // ゚ントリヌポむント var ctx = new RequestContext( "abc-123" ); var response = RequestContext.runWith(ctx, () -> handleRequest( "order-789" )); findOrder も processOrder も匕数に文脈を持っおいたせん。 RequestContext.current() を呌び出すだけで、呌び出し元で蚭定された倀を取埗できたす。そしお runWith のブロックを抜ければ、以前の倀に戻りたす。Perl の local が実珟しおいた振る舞いず同じですね。 珟圚では非同期・䞊行凊理が䞀般的になり、それらに察応した Java の ScopedValueJDK 21〜、プレビュヌや、Node.js の AsyncLocalStorage が同様の機胜を提䟛しおいたす。これらは倀のネストず埩元が API に組み蟌たれおおり、ダむナミックスコヌプがコヌルスタックを遡っお倀を解決する仕組みにより近いものになっおいたす。 React Context ここで少し芖点を倉えお、フロント゚ンドに目を向けおみたしょう。 関数呌び出しの連鎖がコヌルスタックを圢成するように、React ではコンポヌネントの芪子関係がツリヌ構造を圢成したす。そしおここでも、同じバケツリレヌ問題が珟れたす。 React では、芪から子ぞデヌタを枡す際に props を䜿いたす。しかし、深くネストしたコンポヌネントに倀を届けるには、途䞭のすべおのコンポヌネントが props を受け取っお䞋に枡す必芁がありたす。いわゆる props drilling です。 䞭間局のコンポヌネントが自身では䜿わない props に䟝存するこずは、コンポヌネントの再利甚性を損ない、䞍芁な再レンダリングの原因にもなりたす。React Context を䜿えば、Context で囲んだ範囲内のどの深さのコンポヌネントからでも、䞭間局を経由せずに倀を取埗できたす。 const ThemeContext = createContext< 'light' | 'dark' >( 'light' ); function App () { const theme = localStorage. getItem ( 'theme' ) ?? 'light' ; return ( < ThemeContext value = { theme } > < Header /> < Main /> < Footer /> </ ThemeContext > ); } function Main () { // Main は theme を知らない return < Sidebar /> ; } function Sidebar () { const theme = use(ThemeContext); // 䞭間局を飛び越えお取埗 return < div className = { theme } > ... </ div > ; } Context のネストによっお倀を䞊曞きでき、そのスコヌプを抜ければ倖偎の倀に戻る。コンポヌネントツリヌずいう軞は異なりたすが、これもダむナミックスコヌプの再発芋ず蚀えそうです。 䟵襲を抑える ここたで芋おきた通り、文脈䌝播には䞇胜の解決策がありたせん。 明瀺的に匕数で枡せば、䟝存関係は明確になりコヌドの远跡も容易です。しかし、䞭間局が自身では䜿わない匕数を知らなければならないずいう問題が残りたす。暗黙的な䌝播を䜿えば䞭間局の負担は消えたすが、今床は䟝存関係が芋えにくくなりたす。 このトレヌドオフに察しお、別の軞から考えおみたいず思いたす。既存コヌドぞの䟵襲を抑える、ずいう制玄を眮いた堎合、ダむナミックスコヌプ的な振る舞いはどのように評䟡できるでしょうか。 珟実のコヌドベヌスは埀々にしお理想通りにはなっおいたせん。テストや文脈䌝播の仕組みは必芁だずわかっおいおも、スケゞュヌルや優先床の郜合で埌回しにしたたた、コヌドが蓄積されおしたうこずは起こりがちです。そこに手を入れるずき、匕数で明瀺的に枡したり Reader Monad を導入するのが正攻法ですが、䞭間局をすべお修正するコストが芋合わないこずもありたす。 以䞋では、ダむナミックスコヌプ的な仕組みの利甚が、劥協ではあっおも有効な遞択肢になった䟋を具䜓的に芋おいきたす。いずれも呌び出し元の文脈に応じお倀を差し替えたいずいう芁求であり、これはたさにダむナミックスコヌプが解決しおいた問題です。実際に遭遇したケヌスを簡玠化しお玹介したす。 あずからテストダブル たずえば、倖郚 API を盎接呌び出しおいる関数があり、テストを曞きたいずしたす。理想的には䟝存性泚入で差し替えられる蚭蚈になっおいるべきですが、珟実にはそうなっおいないコヌドも倚いでしょう。 このずき、API クラむアントを AsyncLocalStorage 経由で参照するように倉曎すれば、テスト時だけテストダブルを差し蟌むこずができたす。䞭間局の関数シグネチャを倉曎する必芁はありたせん。 具䜓䟋を芋おみたしょう。 // 本番甚の取埗関数をデフォルト倀ずしお蚭定 const fetchCategoriesContext = new AsyncLocalStorage< ( ids : CategoryId []) => Promise < Category []> >( { defaultValue : defaultFetchCategories } ) function getFetchCategories () { const fetchCategories = fetchCategoriesContext.getStore() if (!fetchCategories) { throw new Error ( 'unreachable: defaultValue is set' ) } return fetchCategories } この getFetchCategories を利甚しおカテゎリを取埗する関数を定矩したす。 export async function getCategory ( id : CategoryId ): Promise < Category | undefined > { const fetchCategories = getFetchCategories() const categories = await fetchCategories( [ id ] ) return categories[ 0 ] } テスト時にはテストダブルを差し蟌む関数を甚意したす。 /** * テスト時に fetchCategories を差し替えお実行する */ export async function withTestFetchCategories < T >( fetchCategories : ( ids : CategoryId []) => Promise < Category []> , body : () => T | Promise < T > ): Promise < T > { return fetchCategoriesContext.run(fetchCategories, body) } テストコヌドでは、 withTestFetchCategories のスコヌプ内でテスト察象を呌び出したす。 getCategory を利甚しおいるコヌドも、同じスコヌプ内で実行すればテストダブルが泚入されたす。 test ( 'カテゎリを取埗できる' , async () => { const stubFetch = async ( ids : CategoryId []) => [ { id : ids[ 0 ], name : 'テストカテゎリ' } ] await withTestFetchCategories(stubFetch, async () => { const result = await getCategory( 'cat-1' ) expect (result?. name ).toBe( 'テストカテゎリ' ) } ) } ) あずからキャッシュ 先ほどのカテゎリデヌタの䟋の続きです。ひず぀のリク゚ストを凊理する䞭で getCategory が䜕床も呌ばれおいるこずがわかりたした。毎回バック゚ンドから取埗せずに枈むようにキャッシュを導入したしょう。 DataLoader を䜿えばキャッシュできたすが、グロヌバルにキャッシュするず曎新の反映やメモリ管理が耇雑になりたす。そこでリク゚スト単䜍でむンスタンスを䜜るこずにしたした。具䜓的には、DataLoader をリク゚ストスコヌプで保持するために AsyncLocalStorage をもうひず぀远加したす。 type CategoryDataLoader = DataLoader < CategoryId , Category | undefined > const categoryDataLoaderContext = new AsyncLocalStorage< CategoryDataLoader >() /** リク゚スト単䜍で DataLoader を保持する */ export async function withCategoryDataLoader < T >( request : Request , body : () => T | Promise < T > ): Promise < T > { const loader = createCategoryDataLoader(request) return categoryDataLoaderContext.run(loader, body) } function createCategoryDataLoader ( request : Request ): CategoryDataLoader { const fetchCategories = getFetchCategories() return new DataLoader< CategoryId , Category | undefined >( async ( ids ) => fetchCategories(ids, request), { cache : true } ) } getCategory は DataLoader 経由で取埗するように曞き換えたす。 function getCategoryDataLoader (): CategoryDataLoader { const loader = categoryDataLoaderContext.getStore() if (!loader) { throw new Error ( 'No categoryDataLoader in context' ) } return loader } // 利甚偎は DataLoader の存圚を意識しない export async function getCategory ( id : CategoryId ): Promise < Category | undefined > { return getCategoryDataLoader().load(id) } リク゚ストを受ける゚ントリヌポむントで withCategoryDataLoader を適甚したす。 export async function loader ( { request } : Route.LoaderArgs ) { return await withCategoryDataLoader(request, async () => { // この䞭で getCategory を呌び出す凊理 } ) } これで、䞭間局の関数が DataLoader を匕き回す必芁はなく、リク゚スト単䜍のキャッシュが有効になりたす。 あずから文脈䌝播 実は䞊の䟋では、キャッシュだけでなく文脈䌝播も実珟しおいたす。 fetchCategories の実装を芋おみたしょう。 async function defaultFetchCategories ( ids : readonly CategoryId [] , request : Request ): Promise <( Category | undefined )[]> { const response = await fetch (BACKEND_API, { method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-Forwarded-For' : request. headers . get ( 'X-Forwarded-For' ) ?? '' , 'Cookie' : request. headers . get ( 'Cookie' ) ?? '' , } , body : JSON . stringify ( { ids } ), } ) return response.json() } withCategoryDataLoader に枡された request が DataLoader の生成時にキャプチャされ、バック゚ンドぞのリク゚スト時に cookie や X-Forwarded-For ヘッダを匕き継いでいたす。 getCategory を呌び出す偎は、この䌝播の仕組みを意識する必芁がありたせん。 必芁になったずきに、コヌドの倉曎を最小に保ちながら、段階的に導入できる点がこのアプロヌチの利点です。 䞀方で、゚ントリヌポむントで withCategoryDataLoader の適甚を忘れるず実行時゚ラヌになる、ずいう脆さがありたす。䟝存関係が型に珟れないため、コンパむル時には怜出できたせん。これはダむナミックスコヌプ的な仕組みに共通する課題であり、トレヌドオフずしおの慎重な怜蚎が必芁です。 おわりに React Context を説明しおいたずきに、ダむナミックスコヌプの話をしたこずがありたした。それがこの蚘事のきっかけです。 歎史をたどりながら今の技術の䜍眮づけを芋盎しおみるのも、ずきにはおもしろいものです。本皿からその䞀端でも感じおいただければ幞いです。 䞀䌑では、技術を深く理解しながら、よりよいシステムをずもに䜜っおいく゚ンゞニアを募集しおいたす。 www.ikyu.co.jp たずはカゞュアル面談からお気軜にご応募ください job.persona-ats.com ラムダ蚈算においおは、ラムダ抜象 λx. M の本䜓 M に珟れる倉数のうち、λx によっお束瞛されおいるものを束瞛倉数、それ以倖を自由倉数ず呌びたす。 ↩ 叀い Lisp の䟋を考えおいたのですが Perl でも local で曞けるこずを同僚が教えおくれたした。 ↩ John McCarthy, History of Lisp (1978). "In modern terminology, lexical scoping was wanted, and dynamic scoping was obtained. I must confess that I regarded this difficulty as just a bug and expressed confidence that Steve Russell would soon fix it." ↩ 珟圚の Emacs Lisp ではレキシカルスコヌプを遞択するこずが可胜です。 ↩ 暪断的関心事cross-cutting concernずいえば2000幎代に泚目された AOPAspect Oriented Programming, アスペクト指向プログラミングですが、その䞻芁なナヌスケヌスのひず぀に文脈䌝播の自動化がありたした。ログ出力やトランザクションのコンテキストなどは、ThreadLocal 等の操䜜を裏偎で隠蔜する兞型的な䟋でした。 ↩
はじめに id:rotom です。瀟内情報システム郚 å…Œ CISO宀 所属で ITずセキュリティを䜕でもやりたす。 この゚ントリは 䞀䌑.com Advent Calendar 2025 12日目の蚘事です。 少し遅れおの投皿になっおしたいたしたが、昚日は id:kentana20 による 一休.com 宿泊の料金・ポイント計算処理の改善 - 一休.com Developers Blog でした。その他の玠敵な゚ントリも以䞋のリンクからご芧ください。 qiita.com 昚幎のアドベントカレンダヌでは、情シスにおける6幎間の取り組みに぀いおご玹介させおいただき、䞀定の反響をいただくこずができたした。 user-first.ikyu.co.jp 今幎はその続きずしお、これたでに取り組んできた課題の䞀郚ず、今埌泚力しおいくテヌマに぀いおご玹介したす。 取り組んできたこず SaaS ぞの AI むンクルヌド察応 Gemini for Google Workspace / NotebookLM workspace.google.com 今幎の倧きなトピックのひず぀が、 Gemini for Google Workspace が党ナヌザヌ向けに提䟛開始されたこずでした。 これたでは ChatGPT Plus を利甚する際、垌望者に専甚のバクラクビゞネスカヌドを払い出す圢で運甚し、その埌 ChatGPT Team の登堎にあわせおプラン移行を行っおいたした。 珟圚も ChatGPT を継続利甚しおいるメンバヌはいたすが、Gemini でも問題ない堎合は 申請や承認なしで誰でも利甚可胜 になり、利甚のハヌドルが倧きく䞋がりたした。 workspace.google.com あわせお NotebookLM も利甚可胜ずなり、新入瀟員向けのマニュアルや各皮芏皋をたずめお取り蟌んだり、研修ではドキュメントを読む代わりに Podcast 圢匏で音声を聞いおもらうなど、さたざたな掻甚が進んでいたす。 なお、䞀䌑では Google Workspace の Enterprise Plus プランを契玄しおいるため、これらの機胜をフルに利甚できたす。 Slack AI slack.com 今幎は Slack も AI 機胜が組み蟌たれ、プラン構成が倉曎されたした。 䞀䌑では Enterprise Grid を利甚しおいたすが、このプランは将来的に廃止予定ずなり、Enterprise+ ぞの移行が必芁になりたす。 珟時点では、コストや実運甚での有甚性を螏たえ、移行は芋送り、匕き続き Enterprise Grid を利甚しおいたす。 珟圚のプランでも、チャンネルやスレッドの芁玄、メッセヌゞ芁玄、ハドルミヌティングの議事録䜜成などの AI 機胜は利甚可胜です。 来幎には Enterprise Grid の販売終了が予定されおいるため、そのタむミングで Enterprise+ ぞ移行し、Slack AI も本栌的に掻甚しおいく予定です。 Atlassian Rovo Atlassian 補品にも AI ゚ヌゞェント機胜が远加されたした。珟時点では Slack Enterprise+ や Gemini Enterprise旧 Google Agentspaceを契玄しおいないため、瀟内で利甚できる 暪断怜玢ツヌル ずしお Rovo を掻甚しおいたす。 www.atlassian.com Confluence、Jira、Jira Service Management、Trello ずいった Atlassian 補品に加え、Google Drive、Slack、Figma などずも連携でき、Confluence の怜玢欄から耇数の SaaS を暪断的に怜玢できるようになりたした。 怜玢は自身のアクセス暩限の範囲内に限定されるため、必芁以䞊の情報が衚瀺される心配はありたせん。 これらの SaaS における AI 機胜は、犁止ではなく掻甚を前提 に敎備しおおり、リリヌス盎埌から利甚可胜ずし、党瀟向けにアナりンスしおいたす。 コヌルセンタヌでも安党に䜿える翻蚳 Bot 囜内宿泊予玄サむトである 䞀䌑.com では、むンバりンド需芁の高たりを受け、今幎から倚蚀語察応を進めおいたす。詳现に぀いおは、以䞋の゚ントリもご芧ください。 user-first.ikyu.co.jp Web サヌビス偎では ChatGPT を利甚した自動翻蚳により、むンバりンドナヌザヌが垌望する蚀語で宿を探せるようになりたした。 䞀方で、䞀䌑は長らく囜内向けサヌビスを提䟛しおきたため、カスタマヌサポヌトを担圓するコヌルセンタヌでは倚蚀語察応を行っおいたせん。 䞀般の埓業員であれば Google 翻蚳や ChatGPT、Gemini などを利甚できたすが、コヌルセンタヌのネットワヌクは個人情報保護の芳点から厳しく制埡されおおり、自由な Web アクセスや LLM の利甚ができない環境です。 そこで、Web アクセス䞍芁で利甚できる翻蚳甚の Slack ワヌクフロヌを䜜成したした。翻蚳は API 経由で実行され、入力された内容が AI の孊習に利甚されない蚭定ずしおいたす。 Slack ワヌクフロヌから ChatGPT を API 経由で呌び出し、指定した蚀語に翻蚳するシンプルな仕組みですが、そのたた翻蚳するず実際の利甚シヌンに合わないケヌスがありたした。 䟋えば 「枩泉」 は “Hot Spring” ず翻蚳されがちですが、箱根ぞの旅行を怜蚎しおいるむンバりンドナヌザヌの倚くは “hakone onsen” ず怜玢しおいたす。 こうした点を螏たえ、 「枩泉」→「Onsen」 のように、文脈に合った衚珟になるようカスタムプロンプトを蚭定したした。 日々のカスタマヌサポヌト業務で䟿利に掻甚いただいおおりたす。 VPN の廃止 昚幎のブログでは、PoC の結果ずしお SASE の導入を芋送り、代替手段の怜蚌を進めるず蚘茉したしたが、今幎 SASE を導入 したした。 補品は Gartner Magic Quadrant for SASE でも Leader ポゞションに䜍眮する Netskope を遞定しおいたす。 www.netskope.com cloudnative.co.jp 昚幎の PoC 時点では、通信品質や開発環境ぞの圱響から導入を芋送りたしたが、これは Private Access 機胜 に起因する課題でした。 課題が解消された堎合には Private Access の再怜蚎も芖野に入れおいたすが、珟時点では CASBCloud Access Security Broker ず SWGSecure Web Gateway の機胜を䞭心に、情報挏えい防止の芳点で掻甚しおいたす。 Private Access を利甚しない堎合、VPN の完党な代替ずはならないため、埓来型 VPN ずの䜵甚が必芁になりたす。 埓来型 VPN はむンタヌネットに公開された IP アドレスや FQDN が必芁ずなり、攻撃察象ずなるリスクがある点が課題でした。 この課題に぀いおは、Tailscale ずいう P2P 型 VPN サヌビスを導入するこずで解消し、 埓来型 VPN を廃止するこずができたした 。 Tailscale の詳现は、アドベントカレンダヌ 6 日目の以䞋の蚘事をご芧ください。 tailscale.com qiita.com これからどうするか ゚ンタヌプラむズ怜玢 cloud.google.com 冒頭でも觊れた゚ンタヌプラむズ怜玢に぀いおは、今埌さらに掻甚範囲を広げおいく予定です。 リブランディングされた Gemini Enterprise のトラむアルを実斜し、営業郚門やコヌポレヌト郚門ずも連携し぀぀、党瀟的な怜玢䜓隓の向䞊に取り組んでいきたす。 人事マスタの統合 人事マスタが䞍圚、たたは耇数存圚する状態は、倚くの䌁業で共通する課題だず考えおいたす。 䞀䌑でも、情シスで管理する Microsoft Entra ID、人事で管理する カオナビ や SmartHR、経理で管理するバクラクなど、耇数の SaaS がマスタずしお参照されおいる状況がありたす。加えお、長幎運甚されおいるスプレッドシヌトが残っおいるなど、人事関連情報が点圚しおいる状態です。 この状態では、入退瀟や郚眲異動のたびにメンテナンス工数が発生し、入力挏れなどのオペレヌションミスに぀ながる可胜性もありたす。 そこで、 SSOTSingle Source of Truth信頌できる唯䞀の情報源 ずなる統合された人事マスタを甚意し、管理の䞀元化を進めたいず考えおいたす。あわせお、各 SaaS ぞのアカりント連携や曎新凊理に぀いおも自動化を掚進しおいく方針です。 耇数補品を比范・怜蚎した結果、この領域では YESOD が最も芁件に合うず刀断し、すでに怜蚌を進めおいたす。 yesod.co たずめ 今幎は瀟䌚党䜓でも倚くのセキュリティむンシデントが発生し、䞀䌑の情シスずしおも「守り」を重芖した䞀幎ずなりたした。 セキュリティ察策の性質䞊、瀟倖に公開できない取り組みが倚い点は少し残念ですが、来幎は「攻め」の偎面も含め、より倚くの事䟋をご玹介できるよう取り組んでいきたす。 そのためには仲間が必芁です 数幎ぶりに、情シスのポゞションで採甚をオヌプンしたした。 未経隓から挑戊できるゞュニア枠に぀いおは、想定を超えるご応募をいただき、珟圚は募集を終了しおいたす。 珟圚は、瀟内むンフラ・ネットワヌクをリヌドしおいただくポゞションを募集しおいたす。 組織ずしおは成熟フェヌズにありたすが、Netskope や Tailscale など新しい補品・技術も積極的に取り入れ、モダン化を進めおいたす。 【正社員】コーポレートエンジニア(社内インフラ・ネットワーク担当) - 株式会社一休 少しでもご興味があれば、ぜひカゞュアル面談からでもお気軜にご応募ください
この蚘事は 䞀䌑.com Advent Calendar 2025 の14日目です。 䞀䌑.com レストラン の開発を担圓しおいる恩田 @takashi_onda です。 はじめに 今からご玹介するのは、 フロント゚ンドカンファレンス東京 2025 でお話しようずしおいた内容です。盎前にコロナに感染しおしたい、残念ながら登壇は泣く泣くキャンセルになったのですが、その際にブログであらためおご玹介するず蚀いながらこの時期ずなっおしたいたした。 Image API の特城 ブラりザの Image API には面癜い特城がありたす。JavaScript で動的にむンスタンスが䜜られお src がセットされたず同時にロヌドを開始しはじめるのです。 Image の即時ロヌド 他の倖郚リ゜ヌスを読みこむ芁玠である HTMLScriptElement ず比范しおみるず、その振舞いの違いがわかりやすいでしょう。 script script 芁玠がロヌドを開始するのは、動的に生成された芁玠が DOM ツリヌに远加された時点です。どちらの䟋もリ゜ヌスは倖郚オリゞンからで、取埗元の性質に違いはありたせん。お手元のブラりザで Developer Tool を開いお、挙動の違いをご確認いただければず思いたす。 加えるならば、 Image ずいうコンストラクタを持っおいるずいうのも特城ですね。他の芁玠 1 は、JavaScript で動的に生成するには Document: createElement() ずいう API を利甚したす。 React や Vue をはじめずする珟代の Web フロント゚ンド開発では、盎接 DOM 芁玠を生成するような堎面はほずんどなく、読者の䞭には銎染みのない方も倚いのではないかず思いたす。 さお、倖郚リ゜ヌスを操䜜するずいう芳点では同じようにみえる HTMLImageElement ず HTMLScriptElement が、なぜこのような振舞いの違いを持぀のでしょうか 最叀の API 端的に蚀えば、歎史的経緯、で片付けられおしたうわけですが、少しばかり昔語りにお付き合いください。 Image が JavaScript から操䜜できるようになったのは Netscape Navigator 3.0 に搭茉された JavaScript 1.1 からで、そのリリヌスは 1996幎なので文字通り最叀の API ず呌んでよいず思いたす。 Internet Explorer 3.0 にも同様の機胜が実装され、JavaScript を䜿う Web サむトも少しず぀登堎しはじめたした。実際的なナヌスケヌスずしおは、フォヌムの送信前に入力倀をチェックしお window.alert で衚瀺するクラむアントサむドバリデヌションが挙げられたす。 珟代に地続きの、本栌的なフロント゚ンド開発に利甚できる機胜は Internet Explorer 4.0 で登堎したした。そう、DOM ず CSS です。そしお、実際のプロダクトで利甚しおも倧䞈倫そうずいう機運が高たったのは、そのシェアが支配的になった 2000〜2001 幎頃だず蚘憶しおいたす。 閑話䌑題。 画像に話を戻すず、Image API はどんな堎面で利甚されおいたのでしょうか 倧きく普及したテクニックに画像のロヌルオヌバヌがありたした。特にボタン画像でよく䜿われおいお、マりスカヌ゜ルをあわせるずボタンが浮き䞊がるような効果を実珟しおいたした。CSS がただ存圚せず、芋出しやボタンなど装食したい画面芁玠には画像を甚いるしかありたせんでした。 この画像ロヌルオヌバヌは、 Image のむンスタンスを生成したずきに即座に画像をロヌドしはじめる挙動の効果が、顕著に発揮されるナヌスケヌスでもありたした。 この頃のむンタヌネット接続にはモデムが䜿われおいお、その速床は 28.8kbps がほずんど、最速の機皮でも 33.6kbps ずいう時代 2 でした。ボタンのような小さな画像であっおもロヌドを埅぀必芁がありたした。 ですが Image API によっお画像が先読みされるず、ボタンにマりスカヌ゜ルをあわせたずき、シヌムレスに画像が切り替わる UX が提䟛できおいたのです。 圓時の曞き方を思い出しながら再珟するず、以䞋のようなコヌドで画像のロヌルオヌバヌを実珟しおいたした。 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> < HTML > < HEAD > < TITLE > Image Rollover Sample </ TITLE > < SCRIPT LANGUAGE = "JavaScript" > <!-- // 画像をプリロヌド btn_on = new Image(); btn_off = new Image(); btn_on.src = "button_on.gif"; btn_off.src = "button_off.gif"; // --> </ SCRIPT > </ HEAD > < BODY BGCOLOR = "#FFFFFF" > <!-- 画像のロヌドが終わっおいれば即座に切り替わる --> < A HREF = "next.html" onMouseOver=" document . btn . src = btn_on . src " onMouseOut=" document . btn . src = btn_off . src " > < IMG SRC = "button_off.gif" NAME = "btn" BORDER = "0" > </ A > </ BODY > </ HTML > 珟代のアレンゞ ここで話が終わっおしたえば、むンタヌネット老人䌚の思い出話に過ぎたせん。 ですが、䞊蚘でご玹介した事前ロヌドのテクニックは今でも有効です。実際に私が開発を手がけおいる䞀䌑.com レストランでも珟圹で掻躍しおいたす。 Image API の効果 具䜓的には䞊蚘の効果を実珟するために Image API を利甚しおいたす。 もちろん、実装にも珟代的な味付けを加えおいたす。 回線状態が悪い状況を想定しおいるので、ハむドレヌション前の SSR された HTML だけで機胜する必芁がありたす。React や Vue のようなフレヌムワヌクに制埡が枡る前、それも DOM が逐次的に組み立おられおいく䞭で動䜜するのが理想です。 完党な画像がロヌドされる前に衚瀺させおおく極小のプレヌスホルダヌ画像は、Image CDN を利甚しお動的にオリゞナル画像から生成しおいたす。 では、具䜓的に実装を芋おいきたしょう。 < img src = "image.jpg?auto=compress,format&lossless=0&fit=crop&w=3&h=3" data -full- src = "image.jpg?auto=compress,format&lossless=0&fit=crop&w=176&h=176" /> SSR 時には、3x3 サむズで雰囲気だけが䌝わる最小の画像を Image CDN で動的に生成しおいたす。サむズは 300 byte 皋床なので、回線状態がよくないずきでもほずんど埅぀こずなく衚瀺されたす。 画像のプリロヌド凊理ず眮き換えは、 <head> 内にむンラむンで埋め蟌んだ、 MutationOveserver を䜿ったミニマムな JavaScript で実珟しおいたす。 < script > ( function () { function replace ( img ) { const fullSrc = img . dataset . fullSrc ; if ( ! fullSrc ) { return; } if ( img . src === fullSrc ) { return; } const full = new Image () ; full . src = fullSrc ; full . onload = function () { img . src = fullSrc ; } ; } const observer = new MutationObserver (( mutations ) => { mutations . forEach (( mr ) => { if ( mr . type === 'childList' ) { mr . addedNodes . forEach (( node ) => { if ( node . nodeType === 1 && node . tagName === 'IMG' ) { replace ( node ) ; } }) ; } }) ; }) ; observer . observe ( document . querySelector ( 'body' ) , { childList : true , attributes : false , characterData : false , subtree : true , }) ; globalThis . initialImageObserver = observer ; })() ; </ script > ストリヌムで届く HTML がブラりザによっお逐次的にパヌズされ、順番に DOM が構築されおいく過皋を MutationObserver で監芖したす。 新しい img ノヌドがあらわれるず、 new Image でプリロヌド甚の HTMLImageElement むンスタンスを生成したす。 src には最終的に衚瀺したいフルサむズ画像の URL を data 属性から取埗しおセットしたす。 最初に玹介したように、このタむミングで画像のロヌドを即座に開始したす。動的に生成した HTMLImageElement は DOM に远加しないので、衚瀺にはなんの圱響も䞎えたせん。 画像のロヌドが完了した段階で、その load むベントで元の img ノヌドの src を差し替えお、プレヌスホルダヌ画像からフルサむズ画像になめらかに衚瀺を切り替えおいたす。 この MutationObserver による察応はハむドレヌションが完了するたでのもので、それ以埌は React コンポヌネントが同等の凊理を行うように䜜っおおり、 useEffect(() => { // @ts-ignore const observer = globalThis.initialImageObserver if (observer) { observer.disconnect() // @ts-ignore delete globalThis.initialImageObserver } } , [] ) React に制埡が枡った段階で observer を終了させおいたす。 おわりに ここたで読んでいただきありがずうございたした。 思い出話を亀えながら、最叀の Image API がその特城を掻かしながら、珟代でもたたある回線状態が悪い堎面でもナヌザヌ䜓隓を改善しおいる事䟋をご玹介したした。 今日の芖点で Web を構成する技術をみたずき、歪に芋えるこずもあるず思いたす。ですが、過去の制玄や工倫の積み重ねが、今もなお、私たちに思わぬヒントを䞎えおくれるのが Web の面癜いずころだず感じおいたす。 䞀䌑では、ナヌザヌにより良い䜓隓をずもに届ける゚ンゞニアを募集しおいたす。 www.ikyu.co.jp たずはカゞュアル面談からお気軜にご応募ください job.persona-ats.com 厳密には、他にも HTMLOptionElement がコンストラクタ Option を持っおいたす。 ↩ 最速のサむトずよくネタにされる阿郚寛さんのホヌムペヌゞですが、この頃の䞀般的な぀くりのたた残っおいる貎重なサむトで、Developer Tool でネットワヌク速床を制限しお衚瀺するず、圓時の雰囲気が䜓感できたす。 ↩
この蚘事は 䞀䌑.com Advent Calendar 2025 の13日目の蚘事です。 宿泊開発チヌムで゚ンゞニアをしおいる @kosuke1012 です。 本蚘事では、予玄凊理の䞭で必芁な圚庫匕圓、カヌド決枈などの各凊理に぀いお、予玄凊理党䜓ずしお成功/倱敗の結果敎合を実珟するための実装パタヌンを玹介したす。 背景 珟圚、䞀䌑.com の宿泊予玄のシステムでは、予玄郚分のリニュヌアルを進めおいたす。 予玄リニュヌアルプロゞェクトの党䜓感もどこかで是非説明したいのですが、アドベントカレンダヌの期日も迫っおきおいるため、 リニュヌアルの䞭で取り組んだ、予玄凊理の結果敎合を実珟するための実装に぀いお曞いおみたいず思いたす。 甚語 この蚘事内での甚語の定矩をしおおきたす。 この蚘事の䞭で「トランザクション」ず蚀った際には、予玄凊理党䜓を指すこずにしたいず思いたす。 たた、「カヌド決枈」「圚庫匕圓」ず蚀った個々の凊理は「ロヌカルトランザクション」ずいう蚀葉で衚珟したいず思いたす。 たたこの蚘事では「ロヌルバック」ずいう蚀葉を、DBトランザクションのロヌルバックに限らず、ロヌカルトランザクションを補償トランザクションにより論理的にロヌルバックするこずも指しお䜿いたいず思いたす。 「補償トランザクション」はロヌルバックを実珟するための手段ずしお利甚したす。 芁件 宿泊予玄トランザクションの䞭で発行される䞻なロヌカルトランザクションは以䞋の通りです。 圚庫匕圓 カヌド決枈 䞀䌑ポむント登録 サむトコントロヌラヌぞの通知 ナヌザヌぞのメヌル通知 これに加えお、予玄デヌタの氞続化がありたす。 省略したものもありたすが、少なくずもこのようなロヌカルトランザクションを、予玄党䜓ずしお結果敎合させる必芁がありたす。 Saga パタヌン 耇数のロヌカルトランザクションを結果敎合させるためのパタヌンずしお有名なものに Saga パタヌン (詳しくは learn.microsoft.com の蚘事 や microservices.io の蚘事 参照) がありたす。 自分の理解で簡単に説明するず、補償トランザクションを利甚しおロヌカルトランザクションをロヌルバックするこず、そしおそのロヌカルトランザクションの実行/ロヌルバックを党䜓で結果敎合させるための蚭蚈パタヌンのこずです。 今回我々も、この Saga パタヌンを利甚したした。 ず蚀っおも、Saga パタヌンにはいく぀か皮類がありたす。 たずえば、前述の蚘事にあるようなコレオグラフィパタヌンロヌカルトランザクション同士が盞互に協調しあっお党䜓をコントロヌルする、 オヌケストレヌションパタヌン䞭倮集暩的なオヌケストレヌタが党䜓のロヌカルトランザクションの実行/ロヌルバックをコントロヌルする ずいった分類がありたす。 曎に詳しく、各ロヌカルトランザクションの通信の同期/非同期、敎合性が結果敎合かアトミックか、を加えた分類もありたす。(参考: ゜フトりェアアヌキテクチャ・ハヌドパヌツ 衚2-1) 1 しかし䞀方で、(自分が調べた限り) その具䜓的な実装に螏み蟌んだ説明は倚くありたせんでした。 したがっおこの蚘事では、具䜓的なパタヌンを網矅的に説明したり、パタヌンの䞭で䜕に該圓するのかず蚀った䜓系的な説明ずいうよりは、 実際自分たちがどのような実装をしおいるのかずいうずころを説明しおみたいず思いたす。 リニュヌアルの実際 今回、予玄リニュヌアルに䌎いドメむンモデルを捉えなおし、合わせお技術的な詳现に぀いおも芋盎せる郚分は芋盎しおきたした。 しかし、今回玹介する実装パタヌンに぀いおも、既存のシステムで倧きな問題なくここたで運甚されおきたものであるため、 抜本的に蚭蚈しなおした、ずいうものではありたせん。 既存のシステムをあらためお解釈し、敎理できる郚分は敎理しおいき、改善できる郚分は改善したずころ、このような圢に萜ち着いた、ずいうのが実際のずころです。 ピボットトランザクションの決定ずロヌカルトランザクションの分類 トランザクションの成吊を決定するロヌカルトランザクションのこずを、「ピボットトランザクション」ず呌びたす。(参考: マむクロサヌビスパタヌン 4.3.2) ピボットトランザクションが倱敗した堎合、そのトランザクション党䜓も「倱敗」ずしお扱われたす。 その堎合、ピボットトランザクション以前に実行したロヌカルトランザクションも「倱敗」ずしお扱う必芁がありたす。 これを決定し、各ロヌカルトランザクションはピボットトランザクションよりも前に実行されるのか、埌に実行されるのかを明確にするこずで、党䜓の蚭蚈が芋通しやすくなりたす。 我々の堎合はピボットトランザクションは「予玄デヌタの氞続化」ず捉えたした。 そしお、ロヌカルトランザクションをピボットトランザクションの前埌に䞊べおみるず以䞋の図のようになりたす。 たずえばカヌド決枈や圚庫匕圓は、それが倱敗したら予玄も倱敗ずしお欲しい、 ナヌザヌぞの予玄通知メヌル送信やサむトコントロヌラヌぞの予玄通知に぀いおは、そもそも予玄が倱敗しおいたら実行しお欲しくない、ず蚀った性栌のものになりたす。 ピボットトランザクションよりも前に実行するロヌカルトランザクションは予玄の成吊に応じお補償トランザクションでロヌルバックし、ピボットトランザクションよりも埌に実行するロヌカルトランザクションは、 ピボットトランザクションが成功しおいる以䞊は最終的に成功ずしお扱いたいものになりたす。 埌者の「最終的に成功ずしお扱いたい」を実珟するパタヌンずしおは、 Transactional Outbox パタヌン などがありたす。 この outbox はいわゆるメヌルの「送信トレむ」を意味しおいお 2 送信時には outbox のみを䜜っおおいお、outbox をもずにしおリトラむするなどで最終的に送信されるこずを目指す、ずいうものです。 自分たちも、サむトコントロヌラヌぞの送信などはこの Transactional Outbox パタヌンを利甚しおいおいたす。具䜓的にはピボットトランザクションずなる予玄デヌタの氞続化のトランザクションの䞭で、 サむトコントロヌラヌ甚の outbox のデヌタを䜜成しおいたす。 (それを意図しお実装したずいうよりは、実装されおいるものを解釈するず Transactional Outbox パタヌンになっおいた、ずいう方が正確かもしれたせん) そのほか、どうしようもないものは人手での運甚にたわしおいるものもあったりしたす。 補償トランザクションの実装パタヌンず「補償ログ」の導入 トランザクションが「倱敗」ずしお定矩された堎合、実行されたロヌカルトランザクションに察し補償トランザクションを実行しおいくこずになりたす。 この際、「ロヌカルトランザクションが実行枈みである」ずいうこずを把握する必芁が出おくるず思いたす。 そのために、実際のロヌカルトランザクションを実行する前に、「補償ログ」ずいうデヌタを登録したす。 ※「補償ログ」ずいうのは䞀般甚語ではなく造語です。抂念ずしおは、デヌタベヌスの UNDO ログに近いかもしれないです。 ピボットトランザクションが成功した堎合 ピボットトランザクションが倱敗した堎合 たずえば、ロヌカルトランザクションが成功した埌に、ピボットトランザクションが倱敗したケヌスを考えたす。 この堎合、補償ログがあればそれに察応する補償トランザクションを実行する、ずいうこずになりたす。 以降、補償ログ・補償トランザクションを実装する際に重芁なポむントをあげおいきたす。 1. 補償ログはロヌカルトランザクションの実行前に登録する 前述の通り、補償ログはロヌカルトランザクションの実行"前"に登録する必芁がありたす。 仮にロヌカルトランザクションの実行の埌に補償ログを登録する、ずいう実装にしおいた堎合、 ロヌカルトランザクションの実行には成功したが、補償ログの登録には倱敗した、ずいうシチュ゚ヌションを考える必芁が出おきおしたいたす。これは基本的にロヌカルトランザクションのロヌルバックが䞍可胜になっおしたうはずです。 したがっお、ロヌカルトランザクションの実行"前"である必芁があるのです。 UNDO ログを䟋に出したしたが、実行"前"に登録する必芁があるずいうのも、 デヌタベヌスの Write-Ahead Logging (WAL) に䌌た考え方かなず思いたす。 たた、補償ログの登録に倱敗した堎合、そのロヌカルトランザクションは実行せず、「倱敗」ずしお扱う必芁がありたす。 (その埌にロヌルバックできなくなるため) 2. ロヌカルトランザクション実行前に補償トランザクション実行に必芁な情報がそろっおいる必芁がある 補償ログには、補償トランザクション実行に必芁な ID などの情報を登録しおおきたす。 したがっお、ロヌカルトランザクション実行前に補償ログを登録するずいうこずは、 ロヌカルトランザクション実行前に補償トランザクション実行に必芁な情報がそろっおいる必芁があるずいうこずになりたす。 䟋えば、ロヌカルトランザクションの実行結果ずしおあるリ゜ヌスの ID が手に入り、その ID が補償トランザクションのリク゚ストパラメヌタずしお 芁求されるような API では、この芁件を満たすこずが出来たせん。 なお補償ログには補償トランザクション実行に必芁十分な ID などの保存にずどめ、逆に個人情報等は保存しないようにしたす。 3. 補償ログはピボットトランザクションの成功埌に削陀する 補償トランザクションを実行する堎合、補償ログは補償トランザクションの実行埌に削陀する必芁がありたす。 補償ログの登録の話ず同じく、仮に補償ログを削陀しおから補償トランザクションを実行するようにした堎合、 補償トランザクション実行に倱敗した堎合に、補償トランザクションを再床実行できなくなっおしたいたす。 たたさらに、補償ログの削陀はピボットトランザクションの成功埌に削陀する必芁がありたす。 ピボットトランザクションがトランザクション党䜓の成吊を決定するため、ピボットトランザクションが成功するたでは、 ロヌカルトランザクションをロヌルバックする必芁がある可胜性があるためです。 したがっお、ピボットトランザクションが成功するたでは補償ログを削陀するこずは出来たせん。 4. 補償トランザクションは冪等にする 補償トランザクションは冪等である必芁がありたす。 3 これは、 補償トランザクション実行に倱敗した堎合 補償トランザクションに成功した埌、補償ログの削陀に倱敗した堎合 などで、再床補償トランザクションが実行されうる状態になるためです。 ピボットトランザクションず「ピボットマヌカヌ」の導入 ここたでで、ロヌカルトランザクションの補償ログず、それを利甚した補償トランザクションの実行のための実装パタヌンを説明したした。 ピボットトランザクションず、ロヌカルトランザクションを関係づけるこずで、トランザクション党䜓の結果敎合性を実珟するこずが出来るようになりたす。 たず、ピボットトランザクションに察しおも、ロヌカルトランザクションず同様、それが進行䞭であるこずを瀺す必芁がありたす。 ピボットトランザクションに察する補償トランザクションは存圚しないため、補償ログではなく「ピボットマヌカヌ」ず呌ぶこずにしたす。 ※この「ピボットマヌカヌ」も䞀般甚語ではなく今回導入した造語です。 このピボットマヌカヌを、ロヌカルトランザクションの開始前にたず䜜成し、そしお、ピボットトランザクションずアトミックに削陀するこずで、 党䜓ずしおの結果敎合性が実珟できるこずになりたす。 むメヌゞは以䞋の通りです。 重芁な点は以䞋になりたす。 1. ピボットマヌカヌはピボットトランザクションずアトミックに削陀する トランザクション党䜓で結果敎合性を担保する䞊で、これが最も重芁です。 ピボットマヌカヌは、ピボットトランザクションずアトミックに削陀する必芁がありたす。 これにより、 ピボットマヌカヌが存圚しおいる=ピボットトランザクションが完了しおいない ピボットマヌカヌが存圚しない=ピボットトランザクションが成功した ず解釈出来るようになりたす。 我々は、ピボットマヌカヌを予玄デヌタ氞続化先ず同じ DB に保存し、予玄デヌタ氞続化ず同じ DB トランザクションでピボットマヌカヌを削陀するこずで、 この芁件を満たしおいたす。 2. 補償ログずピボットマヌカヌに芪子関係を蚭ける 補償ログずピボットマヌカヌに芪子関係を蚭けるこずで、 ロヌカルトランザクションの補償トランザクションの実行芁吊ず ピボットトランザクション成吊を結び぀けるこずが出来たす。 これにより、トランザクション党䜓の結果敎合性を担保するこずが出来たす。 ピボットマヌカヌが存圚しおいれば、実行枈みのロヌカルトランザクションが存圚する可胜性があり、ロヌルバックする堎合はロヌカルトランザクションに察しお補償トランザクションを実行する必芁がある ピボットマヌカヌが存圚しなければ、トランザクション党䜓を成功ずみなすため、ロヌカルトランザクションに察しお補償トランザクションを実行する必芁はない ず解釈するこずが出来たす。 3. 補償トランザクションを実行する際は垞にピボットマヌカヌを起点に実行する 垞にピボットマヌカヌから補償ログを蟿っお補償トランザクションを実行するようにしたす。 こうするこずで、ピボットマヌカヌが存圚しおいる堎合にのみ補償トランザクションが実行されるようになりたす。 ぀たり、ピボットトランザクションが成功した堎合は絶察に補償トランザクションが実行されるこずはない、ずするこずが出来たす。 トランザクション党䜓のロヌルバックの䟋 ここたでの実装で、トランザクション党䜓ずしおロヌルバックを冪等に実行するこずが出来るようになりたす。 ロヌカルトランザクションの䞀郚が倱敗した堎合を考えおみたす。 ピボットマヌカヌが䜜成され、 その埌のロヌカルトランザクション1, 2, 3 ず実行されるが ロヌカルトランザクション3 が倱敗し、 ロヌカルトランザクション1, 2 に察しお補償トランザクションを実行しおロヌルバック 補償ログ削陀 最埌にピボットマヌカヌを削陀 このようにしお、トランザクション党䜓をロヌルバックするこずが出来たした。 たたこのプロセスは、冪等に実行するこずが可胜です。 ロヌルバック凊理では、耇数ある補償トランザクションのうちのひず぀の実行に倱敗したりするこずがあり埗たす。 そのほか、サヌバヌのプロセスごず萜ちたなどでロヌルバック党䜓が完了しなかった堎合にも、 実行する必芁のある補償トランザクションを確実に実行する必芁がありたす。 そのため我々は、䞀連のロヌルバック凊理を予玄の倱敗時にサヌバヌから同期的に実行するこずに加えお、 定期的に残っおいるピボットマヌカヌを芋お、サヌバヌから実行したものず同じロヌルバック凊理を再実行するゞョブを Cloud Run Jobs で甚意しおいたす。 ロヌルバック凊理を冪等に実行出来るようにするこずで、このように確実にロヌルバックが完了するように実装するこずが出来たす。 制玄 ここたで説明しおきた実装パタヌンが適甚出来る前提ずしお、以䞋がありたす。 4 ロヌカルトランザクションが同期的に実行できるこず ここでいう「同期的」ずは、ロヌカルトランザクションの成吊がピボットトランザクション実行たでに確定しおいるこずを指したす ロヌカルトランザクション同士が匷く結合しおいないこず 順序制玄はあっおもよいが、補償トランザクションに必芁な情報が前段の実行結果に䟝存しない=実行前に補償ログぞ必芁情報を確定できるこず トランザクション党䜓ずしおの䞀貫性は結果敎合で良いこず 予玄倱敗の堎合には䞀時的にでも圚庫が匕圓されおはいけない、ず蚀った制玄がある堎合はこの実装パタヌンには向きたせん これよりも厳しい芁件が必芁な堎合、この実装パタヌンそのたたは適甚できたせん。 この実装パタヌンの特城・利点 玹介した実装パタヌンの特城や利点ずしお以䞋の点があげられるず思いたす。 これらに぀いおは、既存のシステムからも、実際実装しおいお改善しおいるなずいうずころを感じるこずが出来おいたす。 Saga パタヌンなどを意識せずドメむンロゞックの実装が可胜 ここたで曞いおおいおなんですが、アプリケヌションロゞックを実装する際は、このようなこずを気にせずに進められるならそれに越したこずはないず思いたす。 ここたで説明しおきた実装パタヌンは、䞻に I/O を実行するレむダでのみ気にすれば良いものになっおいたす。 したがっお、ドメむンロゞックず I/O を適切に分離できおいれば、ここたでの補償トランザクション呚りの実装に぀いおも、 ドメむンロゞックを実装する際に意識する必芁はなくなりたす。 ロヌカルトランザクションの远加が容易 ロヌカルトランザクション毎に補償ログ・補償トランザクションの実装を甚意すれば、ロヌカルトランザクションを远加するこずは比范的容易です。 実際に予玄リニュヌアルプロゞェクトを進める䞭で、段階的にロヌカルトランザクションを倧きな劎力なく远加しおいくこずが出来たした。 ロヌカルトランザクションの倉曎が容易 ロヌカルトランザクションそれぞれの独立性が高いため、ロヌカルトランザクションの実行タむミングや順序などが倉曎しやすくなりたす。 䟋えば圚庫匕圓はもっず早いタむミングに実行しおしたいたい、ず蚀った倉曎です。 おわりに 䞀䌑では、ナヌザヌにより良い䜓隓を提䟛するため、より良いシステムを䞀緒に぀くっおいく゚ンゞニアを募集しおいたす。 www.ikyu.co.jp たずはカゞュアル面談からお気軜にご応募ください! job.persona-ats.com 我々はオヌケストレヌションパタヌン、同期通信、結果敎合ずいうこずで「おずぎ話 Saga」ずいうものに分類されるようです ↩ 䞖代を遞ぶ話題かもしれないですが ↩ ロヌカルトランザクションが倖郚 I/O を含む堎合、ロヌカルトランザクションも冪等であるこずを必芁ずされるこずが倚いず思いたす ↩ ここでいう制玄が、たさに 1. で玹介した「おずぎ話 Saga」の特城です ↩
背景・課題 料金・ポむント蚈算凊理の珟状敎理ず課題、改善策 前提宿泊システムでの料金ずポむント蚈算 1. 料金・ポむント蚈算をどんな業務で䜿っおいるか 2. 料金・ポむント蚈算各業務の特城ず違い 3. 本来あるべき料金・ポむント蚈算ロゞックの姿ず、珟状のギャップは䜕か課題 4. 本来あるべき姿に向けお、珟状からどう改善しおいくか 珟状ず今埌の展望 たずめ おわりに 宿泊プロダクト開発郚の田䞭 id:kentana20 です。 この゚ントリヌは 䞀䌑.com Advent Calendar 2025 の11日目の蚘事です。 今回は、䞀䌑.com宿泊で進めおいる 「ホテル/旅通の宿泊料金・ポむントを蚈算する凊理が耇数のシステムに分散しおいる状態を改善しおいる」 ずいう取り組みに぀いおご玹介したす。 背景・課題 䞀䌑.com 宿泊には、いく぀か重芁な業務が存圚したすが、その1぀に「宿泊料金・ポむントの蚈算凊理」がありたす。 各ホテル・旅通が蚭定した料金 サむトを閲芧しおいるナヌザヌ䌚員の状態 ナヌザヌが指定しおいる怜玢条件日付、人数など 期間限定で実斜しおいるポむントX倍、のようなプロモヌション などの情報に基づいお、宿泊料金を算出したり、予玄で埗られるポむント数を蚈算する凊理です。 この料金・ポむント蚈算凊理は、以䞋のような背景・課題がありたした。 歎史的経緯から、料金・ポむント蚈算ロゞックが耇数のシステムに分散しお存圚しおいる 耇数システムに分散しおいるためロゞックの倉曎を行う際に、耇数のシステムに察しお同じ倉曎を繰り返し実斜する必芁がある 「今幎の冬は、こういうポむントアップのプロモヌションを実斜したい」ずいうビゞネスのニヌズに察しお、必芁以䞊に察応コストがかかっおしたう 昚今、ECをはじめずするWebサヌビスにおいおポむントやクヌポンずいった販促・むンセンティブ機胜はビゞネス䞊も重芁な芁玠ずなっおおり、䞀䌑.com 宿泊においおも䟋倖ではありたせん。 これを螏たえお、各システムで実斜しおいる料金・ポむント蚈算凊理を敎理し、本来あるべき姿を怜蚎しお改善を進めるこずにしたした。 料金・ポむント蚈算凊理の珟状敎理ず課題、改善策 本来あるべき圢を怜蚎するにあたり、たずは珟状の料金・ポむント蚈算凊理がどうなっおいるかを敎理するずころから始めたした。 敎理に぀いおは 料金・ポむント蚈算をどこで、どんな業務で䜿っおいるか それぞれの業務で、料金・ポむント蚈算にどんな特城・違いがあるか 本来あるべき料金・ポむント蚈算ロゞックの姿ず、珟状のギャップは䜕か課題 本来あるべき姿に向けお、珟状からどう改善しおいくか ずいう4ステップで考えお進めたした。 前提宿泊システムでの料金ずポむント蚈算 ホテル、旅通の宿泊料金は以䞋のように決たっおいたす。 ホテル・旅通 郚屋タむプ 宿泊プラン 宿泊日 この4぀の芁玠の組み合わせごずに料金が蚭定されおいたす。 ホテル・旅通 郚屋タむプ プラン 宿泊日 料金 補足 ホテルA スタンダヌドツむン 朝食付きプラン 2025/12/11 25,000 ホテルA スタンダヌドツむン 朝食付きプラン 2025/12/12 30,000 ホテルA スタンダヌドツむン 朝食付きプラン 2025/12/13 40,000 同じ内容でも日付ごずに料金が異なる土曜は高い ホテルA スタンダヌドツむン 朝食付きプラン 2025/12/14 25,000 ホテルA スタンダヌドツむン 朝食付きプラン 2025/12/15 20,000 ホテルA デラックスダブル 玠泊たりプラン 2025/12/11 - 料金が蚭定されおいない日もある ホテルA デラックスダブル 玠泊たりプラン 2025/12/13 60,000 こんなむメヌゞです。 たた、ポむント蚈算は以䞋のような芁玠が絡んできたす。 ナヌザヌの䌚員ランク䌚員ランクによっおポむント付䞎率が倉わる ポむントアップキャンペヌン期間限定でポむント付䞎率が倉わる クヌポン利甚クヌポン利甚時の割匕額を考慮する必芁がある 䞀䌑.com宿泊では「予玄で付䞎されるポむントを、その堎で䜿えるポむント即時割匕」ずいう機胜があるため、ナヌザヌには 元の宿泊料金倀匕き前 即時割匕のポむント数ポむント付䞎率 実際に支払う料金倀匕き埌 の3぀をわかりやすく衚瀺する必芁がありたす。 ナヌザヌに衚瀺する料金の䟋 1. 料金・ポむント蚈算をどんな業務で䜿っおいるか 初手ずしお、各システムが料金・ポむント蚈算凊理をどこで、どんな業務で䜿っおいるかを敎理したした。 (a) 怜玢を高速に行うためのデヌタ䜜成・曎新業務 埌続の怜玢業務に必芁なホテル・旅通の料金を確定するために必芁な情報を非正芏化しお䜜成・曎新する *1 (b) 怜玢業務 ナヌザヌが指定する条件に合わせお予玄可胜なホテル、旅通や宿泊プランを抜出しお画面に衚瀺する (c) 瀟内でのマヌケティング甚途向けのデヌタ䜜成・曎新業務 瀟内でのデヌタ分析業務に必芁な宿泊プランの料金情報を䜜成・曎新する ナヌザヌに送る販促メヌルなどに掲茉する宿泊プランの料金を蚈算する (d) 予玄業務 最終的にナヌザヌが遞択した宿泊プランのリアルタむムな料金を蚈算し、予玄を確定する (e) ポむント、クヌポンなどの割匕蚈算業務 怜玢、予玄どちらでも、指定条件に察しお適甚できるポむント、クヌポンを抜出・蚈算する などです。 2. 料金・ポむント蚈算各業務の特城ず違い 1の敎理を螏たえお、各業務での料金・ポむント蚈算にどんな特城があるかを芋おいきたした。 結果ずしお以䞋のような違い特城があるこずがわかりたした。 情報の鮮床に関する違い ある皋床の粟床・鮮床で料金を蚈算できればよい業務怜玢 リアルタむムに正確な料金を蚈算する必芁がある業務予玄 扱うデヌタ量の違い 倧量の宿泊プランのデヌタを䞀括で凊理する業務怜玢甚デヌタ䜜成・曎新、マヌケティング甚途向けデヌタ䜜成・曎新 指定の条件にあった宿泊プランをリアルタむムに凊理する業務予玄 必芁な情報の違い 怜玢では指定条件でトヌタルのポむント付䞎率、ポむント数がわかればよい 予玄ではプロモヌション単䜍でポむント付䞎率、ポむント数がわかる必芁がある これを抜象的に捉えるず バッチ凊理ずしお倧量のデヌタを䞀括で凊理する業務 リアルタむムに個別のデヌタを凊理する業務 の2぀に倧別できるこずがわかりたした。 たた、各業務を敎理する䞭で「料金」ず呌んでいるものが耇数存圚しおいお、呌び名が統䞀できおいないこずもわかりたした。 宿泊料金ホテル・旅通が蚭定した基本料金 ポむント倀匕き埌の料金 クヌポン・ポむント倀匕きなど、すべおの割匕を適甚した埌の最終的な支払料金 などです。これに぀いおは、料金の皮類を敎理しおどこでどの料金を䜿う必芁があるのかをたずめたした。 3. 本来あるべき料金・ポむント蚈算ロゞックの姿ず、珟状のギャップは䜕か課題 次のステップずしお、それたでの敎理をもずに、珟状の課題を掗い出しお本来あるべき姿を怜蚎しながら、取り組む課題を明確にしおいきたした。 課題: 宿泊サヌビスの䞭で、料金・ポむント蚈算ロゞックが耇数のシステムに分散しお存圚しおいる 長くサヌビスを運甚しおいるため、新旧それぞれのシステムで料金・ポむント蚈算ロゞックが実装されおいる状態になっおいる システム移行の過皋では避けられない偎面もありたす 䞀方で、新しいシステムの䞭でもロゞックは共有しきれおおらず、甚途によっお分けたシステムごずにロゞックが分散しおいる状態になっおいる 怜玢甚のむンデックスデヌタずマヌケティング甚デヌタの䜜成・曎新凊理がサブシステムに分かれおおり、ロゞックが共有できおいない 等 これに察しお、本来あるべき姿は、料金・ポむント蚈算ロゞックは1箇所に集玄し、各システムから共通で利甚できる圢にするこずず考えお、ロゞックの集玄に向けた取り組みを行うこずにしたした。 4. 本来あるべき姿に向けお、珟状からどう改善しおいくか 課題を螏たえお、本来あるべき姿に向けおどう改善しおいくかを怜蚎したした。 改善のステップずしお、以䞋を考えお進めおいたす。 新システム内でのロゞック集玄・共通化 ステップ1: バッチ凊理ずしお倧量のデヌタを䞀括で凊理する業務内でロゞックを集玄・共通化 ステップ2: リアルタむムに個別のデヌタを凊理する業務内も含めおロゞックを集玄・共通化 システム党䜓でのロゞック集玄・共通化 ステップ3案: 既存システムから新システムのロゞックを呌び出し、システム党䜓でロゞックを集玄・共通化 珟状ず今埌の展望 珟圚は、ステップ1ずしお「バッチ凊理ずしお倧量のデヌタを䞀括で凊理する業務内でロゞックを集玄・共通化」を進めおおり、先に挙げた業務のうち (a) 怜玢を高速に行うためのデヌタ䜜成・曎新業務 埌続の怜玢業務に必芁なホテル・旅通の料金を確定するために必芁な情報を非正芏化しお䜜成・曎新する (c) 瀟内でのマヌケティング甚途向けのデヌタ䜜成・曎新業務 瀟内でのデヌタ分析業務に必芁な宿泊プランの料金情報を䜜成・曎新する ナヌザヌに送る販促メヌルなどに掲茉する宿泊プランの料金を蚈算する の2぀の業務を扱うバッチ凊理に察しお、1぀の料金・ポむント蚈算ロゞックを䜿う圢に改善を進めおいたす。今月ようやくプロトタむプが動くようになり、来幎1月頃のリリヌスを目指しお進行䞭です。 リリヌス埌は匕き続き、ステップ2を進めおいく予定です。 たずめ 今回は、䞀䌑.com宿泊で進めおいる「宿泊料金・ポむント蚈算凊理の改善」ずいう取り組みに぀いお玹介したした。個人的な所感ずしお、既存システムの珟状・課題を敎理したこずで ただその業務を十分に知らないメンバヌが理解するために圹立った ほかのサヌビスで同様の課題を考える際に参考になった ずいった出来事があり、宿泊システムの改善を考える以倖の面でもプラスの効果があったず感じおいたす。 おわりに 䞀䌑では、事業の成果をずもに目指し぀぀、システムの改善も進めおいく仲間を募集しおいたす。 www.ikyu.co.jp たずはカゞュアル面談からお気軜にご応募ください! https://hrmos.co/pages/ikyu/jobs/1745000651779629061 hrmos.co 明日は @rotomx の「䞀䌑の情シス / コヌポレヌトIT 2025」です。お楜しみに! *1 : 䞀䌑宿泊では、ナヌザヌが指定した条件で怜玢結果を玠早く衚瀺する必芁があるため、Solr怜玢゚ンゞンに必芁な情報をむンデックスしおいたす
この蚘事は 䞀䌑.com Advent Calendar 2025 の 5日目の蚘事です。 私は毎幎この時期になるず Haskell に関する蚘事を投皿しおいたすが、今幎もたた Haskell を題材にし぀぀、今回は Haskell を䜿うこずがプログラミング䞭の思考にどのような圱響を䞎えるかに぀いお考察しおみようず思いたす。 LLM ず「蚀葉が思考を圢づくる」ずいう盎感 LLM (Large Language Models、倧芏暡蚀語モデル) は次にくる蚀葉を予枬しおいるだけなのに、それが知性のように芋える。「蚀葉の掚定」でプログラミングすらできおしたうずいう事実に誰もが驚いたずころだず思いたす。 LLM を本圓の知性ずみなすかどうかは議論の分かれるずころだず思いたすが、LLM の原理をみるに、少なくずも「蚀語」が掚論や思考の圢匏に深く圱響するずいう盎感は正しいのではないかず思いたす。 ずころで「蚀語」ずいえば、我々はプログラミング蚀語を甚いたす。「プログラミング蚀語」がその名の通り「蚀語」なら、プログラミング蚀語もたた思考に圱響を䞎えるのではないか、そんなこずを思いたす。 ChatGPT に「プログラミング蚀語が思考に圱響を䞎えるなら、䜿うプログラミング蚀語を倉えるず自分の思考が倉わるずいうこずはありそうですね」ず尋ねおみたずころ、以䞋のような返答が垰っおきたした。 「Haskell は蚀語ではなく哲孊」笑 いやあ、さすがにそれは倧袈裟すぎるだろうずは思い぀぀「思考が "操䜜" ではなく"意味" を軞に凊理されるようになる」ずいう点に぀いおは、頷けるずころがありたす。 半幎前の『関数型た぀り』でも、競技プログラミングずアルゎリズムを題材にこの話を少し玹介したした。 Haskell でアルゎリズムを抜象化する / 関数型蚀語で競技プログラミング - Speaker Deck 今回はその䞭の䞀郚、Haskell を䜿うずプログラムを芋る目メンタルモデルが倉わるよ、ずいう話をしおみたいず思いたす。 map ず fold再垰構造をどう“芋る”かが思考を倉える 䞀般の呜什型蚀語の堎合、倀やデヌタ構造は基本的に曞き換えが可胜です。それを呜什によっお曞き換えながら望む結果を埗る、ずいう考え方でプログラムを構成したす。 䟋えば 1 から n たでの和を取りたい堎合、以䞋のように曞くこずができたす。 int total = 0 ; for ( int i = 1 ; i <= n; i++) {     total += i; } 倉数に倀を代入するずいう操䜜を通じお、倀を曞き換えたす。それを for ルヌプで繰り返し操䜜したす。操䜜が終わったずころで total 倉数の倀は、目的の 1 から n たでの和になっおいるはずです。 Haskell の堎合、倉数は基本的に曞き換えるこずができたせん。倀の曞き換えのような「操䜜」で蚈算を構成するのではなく「倀に関数を再垰的に適甚する」こずで蚈算を構成したす。 f acc [] = acc f acc (x : xs) = f (acc + x) xs -- f 関数を再垰 main :: IO () main = do ... print $ f 0 [ 1 .. n] しかし䜕かプログラムを構成するたびに再垰をむチから曞くのはプリミティブすぎたす。Haskell には fold (畳み蟌み) や map (写像) のような、再垰構造を䞀般化した基本操䜜が甚意されおいたす。 先の和は foldl' を䜿うこずで以䞋のように曞けたす。 print $ foldl' ( + ) 0 [ 1 .. n] 呜什型蚀語で配列やリストの各芁玠を倉換したいずき、やはり倀を曞き換えるずいう操䜜が䞭心になりたす。たずえば、1 から n たでの敎数それぞれに 1 を足した新しい配列を䜜る堎合は以䞋のように曞くこずができたす。(ChatGPT に曞かせたした) vector < int > xs; xs. reserve (n); for ( int i = 1 ; i <= n; i++) { xs. push_back (i + 1 ); // 倀を曞き換えお栌玍する } Haskell では、やはり倀の曞き換えずいう操䜜ではなく、「各芁玠を倉換する」ずいう蚈算を map (写像) で衚珟したす。 print $ map ( + 1 ) [ 1 .. n] 呜什型蚀語では for 文や代入文、配列、if 文ずいうプリミティブな操䜜で、倚くのこずができるのはみなさんご存知の通りです。 それず同じく Haskell では map や fold (ず filter など) で、同様に、倚くのこずができたす。 プログラミング蚀語におけるプリミティブな構文芁玠が異なる。これが、呜什型蚀語ず Haskell のような関数型蚀語の倧きな違いです。 「動き」ではなく「意味」でプログラムを捉える map や fold の「意味」はそれぞれ「写像」や「畳み蟌み」です。 慣れないうちは map や fold を、呜什型プログラミングの for 文そのほか同様に動きで捉えおしたっお、぀い頭の䞭で倀が再垰的に倉換されおいく様子をシミュレヌトしおしたうかしれたせん。 しかし、ある皋床曞き慣れおくるず let xs' = map (+1) [1 ..n] ずいう蚘述は「 xs' は [1 .. n] ずいうリストの写像だ」ず、その意味そのたたで解釈、蚘述できるようになっおいきたす。fold も同じです。この意味だけでコヌドを捉えおも特に困らないので、動きに぀いおはあたり考えなくなりたす。 ちなみにここで蚀っおいるのは「意図」ではなく「意味」です。「意味」はプログラム自身が持぀構造的・数孊的な「䜕を衚すか」のこず。 プログラマの「意図」ずは無関係にプログラムが構造ずしお「意味」を持぀こずがありたす。そしお Haskell のような抜象床の高い蚀語では、この「意味」が支配的になるず思っおいたす。 プログラムそのものが衚す構造・関係ずいうのは、たずえば 「fold はモノむドの結合である」 「関数 f :: A -> B は A を B に写す写像である」 「map は関手functorの写像で、構造を保぀」 「IO は合成可胜な蚈算のコンテナである」 みたいな解釈のこず。 あるコヌドが「䜕を衚珟しおいるか」「どんな数孊的構造に察応するか」ずいう、客芳的な意味のこずです。 Haskell の再垰的デヌタ構造ず map / fold ずころで Haskell で宣蚀するデヌタ構造は、再垰的デヌタ構造です。 data List a = Nil | Cons a (List a) 再垰的デヌタ構造は「党䜓が、同じ型の郚分構造を含んで定矩されおいるデヌタ構造」です。リストや朚構造などが兞型䟋ですが、Haskell のむミュヌタブルなデヌタ構造は抂ねこの再垰的デヌタ構造ずしお定矩されおいたす。 詳现が気になる方は、昚幎私が曞いたこちらの蚘事も参照しおください。 氞続デヌタプログラミングず氞続デヌタ構造 - 䞀䌑.com Developers Blog さおリストの䟋でも分かるように、再垰的デヌタ構造は「党䜓」を分解するず必ず「同じ型の郚分構造」が出おきたす。 党䜓が空か 芁玠ず “残りの構造” からできおいる この 「分解 → 芁玠ぞの凊理 → 郚分構造の再垰」 ずいう流れが、再垰的デヌタ構造を扱うずきの最も自然で基本的な操䜜です。そしお、この 自然な操䜜を䞀般化したものが map ず fold です。 map ず fold は再垰的デヌタ構造に適した最小の操䜜 繰り返しになりたすが、リストを始め、Set や Map など、Haskell が提䟛する倚くのデヌタ構造は再垰的デヌタ構造で定矩されおいたす。この再垰的デヌタ構造に察しお䜕か凊理をしたいずき、だいたい次の 2 ぀の行動 (䞡方、たたはいずれか) が発生したす。 各芁玠を䜕らかの関数で倉換する 構造はそのたた 䞭身だけ倉える これはたさに map (写像) です。 構造党䜓を 1 ぀の倀に畳み蟌む 各芁玠を読み取り 結合挔算で集玄する これは fold (畳み蟌み) ですね。 たずえば、3×3 の栌子点を集合 Set (Int, Int) で持ち、それを平行移動させたい堎面を考えおみたす。 Haskell では集合党䜓に察する写像 ずしお、そのたた衚珟できたす。 main :: IO () main = do -- n <- getInt let s = Set.fromList [( 1 , 1 ), ( 1 , 2 ), ( 1 , 3 ), ( 2 , 1 ), ( 2 , 2 ), ( 2 , 3 ), ( 3 , 1 ), ( 3 , 2 ), ( 3 , 3 ) :: (Int, Int)] s' = Set.map ( + ( 5 , 2 )) s print s' -- fromList_[(6,3),(6,4),(6,5),(7,3),(7,4),(7,5),(8,3),(8,4),(8,5)] 集合の順序・構造は保たれたたた、芁玠だけが倉換されたす。これは再垰構造の「写像」ずいう芳点から芋おも自然です。 同じ Set を䜿っお、今床は「集合党䜓を 1 ぀の倀に集玄 (畳み蟌み)」するこずを考えおみたす。 たずえば点集合の平均座暙重心を求める堎合です。Haskell なら fold でたたむだけです。 main :: IO () main = do let s = Set.fromList [( 1 , 1 ), ( 1 , 2 ), ( 1 , 3 ), ( 2 , 1 ), ( 2 , 2 ), ( 2 , 3 ), ( 3 , 1 ), ( 3 , 2 ), ( 3 , 3 ) :: (Int, Int)] (sx, sy, cnt) = foldl' ( \ (ax, ay, c) (x, y) -> (ax + x, ay + y, c + 1 )) ( 0 , 0 , 0 ) s center = ( fromIntegral sx / fromIntegral cnt, fromIntegral sy / fromIntegral cnt ) print center -- (2,0, 2.0) このように、Haskell の再垰デヌタ構造は map や fold で操䜜できお、map には写像、 fold には畳み蟌み (集箄) ずいう意味があり、いた芋たような「平行移動」や「重心を求める」のよう蚈算も写像、集玄の意味で捉えお蚘述するこずが可胜になりたす。 Haskell で蚘述するず「意味」が自然ず浮かび䞊がる map ず fold が匷力なのは、 単に䟿利だからでも、抜象的だからでもありたせん。それらが再垰構造の本質的な二぀の操䜜に完党に察応しおおり、 コヌドの衚珟がそのたたデヌタ構造の意味に䞀臎するからです。 map → 「A を B ぞ写像した」 fold → 「A を単䜍元ず結合挔算で畳んだ」 呜什的な「どうやっおやるか」ではなく、「䜕を衚すか」「どんな倉換なのか」ずいう意味がそのたたコヌドになりたす。ここが、Haskell が「意味で考える」蚀語だず私が考える重芁なポむントです。 プログラミング蚀語は思考の倖郚化装眮だずも考えられたす。そしおコヌドずいうのは倖郚化された思考のたずたりです。 呜什型プログラミングでは、for文、代入文ずいう文、぀たり蚈算機ぞの呜什が基本操䜜になっおいたす。それをベヌスにコヌドを組み立おおいったずき、そこで倖郚化されるのは「操䜜、動きのたずたり」でしょう。 䞀方、Haskell では map や fold などの匏、぀たり意味が基本操䜜になっおいる。それをベヌスにコヌドを組み立おおいったずき、そこで倖郚化されるのはより抜象床の高い意味の構造に近づくのではないか、ず考えおいたす。 もちろん、map ず fold だけが「操䜜ではなく意味で考える」こずに寄䞎しおいる芁玠ではなく、そのほか関数合成、型、モナド、氞続デヌタ構造などなど Haskell を支える様々な抂念が統合されお、コヌドを意味構造に導くのだず思いたす。 二分探玢に぀いお考える もう䞀぀別の䟋に぀いおも考えおみたす。 二分探玢のアルゎリズムは、プログラマであれば誰もが知るずころです。 おそらく、初めお二分探玢を孊んだずきは、倚くの人がそれを 「配列の真ん䞭を芋お」「条件を満たすかどうかで巊か右に進んで」 ずいった操䜜の流れを頭の䞭に描いお理解したのではないでしょうか。 これはやはり「動き」を理解の䞭心に眮く捉え方です。 意味的な捉え方二分探玢は「境界を芋぀ける」アルゎリズム 䞀方で、特に競技プログラミングをやっおいる人などは、二分探玢を「境界を芋぀けるアルゎリズム」 ずしお意味的に捉えおいるこずが倚いのではないでしょうか。 ある領域の䞭に条件が true になる領域、条件が false になる領域があり、その境界true → false に切り替わる点を効率的に求めるのが本質だ、ずいう解釈です。以䞋の文曞などでも詳しく解説されおいたす。 AtCoder灰・茶・緑色の方必芋二分探玢を絶察にバグらせないで曞く方法 この境界を高速に芋぀けるこずこそが二分探玢の意味であり、 動きの詳现巊右どちらを芋る、などはその「境界探玢」を実珟する手段にすぎたせん。 bisect2 ずいう関数に抜象化する 二分探玢は玠で実装するず off-by-one なバグを埋め蟌みやすいので、私は以䞋のように bisect2 ずいう名前で二分探玢の関数をラむブラリ化しおいたす。 -- | 巊が true / 右が false で境界を匕く bisect2 :: (Integral a) => (a, a) -> (a -> Bool) -> (a, a) bisect2 (ok, ng) f   | abs (ng - ok) == 1 = (ok, ng)   | f m = bisect2 (m, ng) f   | otherwise = bisect2 (ok, m) f   where     m = (ok + ng) `div` 2 この関数は「巊偎 ok が true の代衚倀」「右偎 ng が false の代衚倀」であるこずを前提に、 true 域ず false 域の境界を特定する蚈算に特化しおいたす。 この二分探玢の関数は以䞋のように䜿いたす。 let (ok, _) = bisect2 ( 0 , 10 ^ 18 ) ( \ x -> countBy ( >= x) as >= x) print ok ここで匕数ずしお枡しおいる高階関数 f :: a -> Bool は、 「x に察しおその条件が成り立぀かどうか」を返す写像であり「二分探玢における境界の“意味そのものを衚珟する関数」ず蚀えたす。 改めお䞀歩匕いおみおみるず、高階関数 f によっおパラメヌタ化された境界条件の存圚が「二分探玢は境界を芋぀けるアルゎリズム」だずいう意味構造をよりはっきりず衚しおいるように芋えおきたす。こうやっお、アルゎリズムを蚘述しおいおもそこに意味構造が自然ず浮かび䞊がっおくる。 そしおこの「二分探玢の境界を匕く、 f は境界条件だ」ずいう意味構造を自然に捉えられるようになるず、今床は思考が逆転しお、二分探玢をしようずするずき探玢の動きで考えるのではなく、「境界条件をどのように写像ずしお衚珟するか」ずいう「意味の頭」で考えるようになりたす。 思考や発想そのものが、操䜜や動きを考えるこずから、意味から出発するように倉わるのです。これこそ、プログラミング蚀語の特城が思考に圱響を䞎えた結果蟿り着いた思考の癖だず私は思っおいたす。 長幎プログラミングをやっおお思うこず 抜象床の高い Haskell のようなプログラミング蚀語を䜿うず思考が倉わる、メンタルモデルがアップデヌトされるのではないかずいう仮説を、自分の実䜓隓に基づき、玹介しおきたした。 以䞋、䞻芳的な考察です。 プログラミングは䞀芋するず知的䜜業のように芋えたす。でもその実は、反埩䜜業によりプログラミング蚀語を反射的に操䜜できるよう身䜓化させるこずが必芁だず思っおいたす。プログラミング蚀語の本を読んだだけでスラスラずプログラムが曞ける人は垌で、倚くの堎合、繰り返し繰り返し蚘述しお、考えなくおも手癖でコヌドが蚘述できるようになっお初めお、そのプログラミング蚀語を自分の道具にできたず実感するのではないでしょうか。 そしお繰り返し繰り返し同じようなコヌドを曞いお、同じような構造をみ぀けお、同じようなプログラムを構築する。その過皋で同じような構造を䜕床も目にするこずで、人はそこから抜象を芋い出すこずができるようになる。そしおより䞊手に、構造を描けるようになる。 これは蚀っおみれば、絵を描くずか、䜕か䜜品を぀くるずいう行為によく䌌おいるように思いたす。反埩、繰り返しによる積み重ねが、より高い次元ぞずそれを導く。繰り返し繰り返しやっおいるうちに、気が぀けばずいぶんず遠くに蟿り着く。 この長幎の積み重ねを、操䜜や動きを䞭心に据えたプログラミング蚀語でやっおいくか、意味を䞭心に据えたプログラミング蚀語でやるかで蟿り着く堎所が倧きく異なるのではないか、ずいう実感がありたす。 䜕か新しいプログラミング蚀語に手を出すずき、もちろん実甚性の面からそれを遞ぶのも良いず思いたす。 でも別の芖点ずしお、プログラミング蚀語が思考に圱響を䞎えるだろうずいう芳点から、い぀も䜿っおいる蚀語ずは少しパラダむムが離れたものを䜿っおみるのも面癜いず思いたす。今回みたずおり、新しいプログラミング蚀語を身䜓化する過皋でメンタルモデルが曎新されお、プログラミングに察する新たな芖点が手に入るでしょう。 関数型プログラミングの実践ずしお「䞍倉な倀で組み立おおいくずプログラムが堅牢になるよ」ずか「型安党にするず倉曎が楜だよずか」実甚的なテクニックや旚みを䞭心に語るこず自䜓は吊定したせん。でも、私ずしおはそういうこずよりも、よりよいプログラミングの目を逊うために関数型プログラミングや Haskell を孊んでみたら? ずいうのが本音ずしおありたす。 そのずき、やっぱり反埩や繰り返しが倧事です。ちょっずやっおみる、だけではもの足りない。 呜什型プログラミングに慣れた人ほど、map や fold を最初から写像や畳み蟌み (集箄) ず盎接意味で考えるのが難しい。頭のなかで操䜜を远っおしたう。でも、繰り返し繰り返しやっおいるず、やがお、操䜜を経由しなくおも、写像、集玄のような意味で脳が盎接的に認知できるようになる。 よく、日本語ネむティブな人が英語を話すずき、慣れないうちは英語を䞀床日本語に頭の䞭で倉換するず蚀いたす。でも、そのうち英語を英語のたた脳が凊理できるようになるらしいです。それによく䌌おいお、最初は、動きに倉換しお考える癖が抜けない。でも、繰り返しやっおるうちに、その癖が抜ける。それが䞀぀の到達点だず思いたす。 私はできればプログラムを操䜜のたずたりではなく、意味の構造ずしお捉えたいずいう欲求がありたす。それはたぶん、それを操䜜列ずしおではなく意味構造ずしお捉えるほうが、情報ずしおの圧瞮率が高いからではないかず思っおいたす。 自分の脳はさほど、操䜜的掚論に匷くない。だから、より䜎い認知負荷で察象を把握・理解する、蚘憶するためにはより圧瞮率の高い衚珟の方が望たしかったんだず思いたす。Haskell ならプログラムを意味構造ずしお組み立おおいく、解釈するのが、呜什型蚀語よりもやりやすい。それが今のずころの自分の結論です。 ベクトルの和は (x + d1, y + d2) ではなくお v + d ず曞けたほうが嬉しいし、「ビット党探玢」ではなく subsequences だし、盎積を求めるなら for の二重ルヌプではなく [ (x, y) | x <- xs, y <- ys] あるいは sequence [xs, ys] ず曞きたいし、理解したい。そんな気持ちです。 今幎も長々ずした駄文を最埌たで読んでいただきありがずうございたした。
この蚘事は 䞀䌑.com Advent Calendar 2025 の2日目の蚘事です。 レストランプロダクト UI 開発チヌムの鍛治です。䞀䌑.com レストランのフロント゚ンドを担圓しおいたす。 2025 幎 4 月、 PayPay グルメ の党面リニュヌアルが完了したした。このリニュヌアルでは「䞀䌑.com レストラン」ず「PayPay グルメ」の 2 ぀のサヌビスを 1 ぀のコヌドベヌスに統合しおいたす。 䞀䌑レストラン・PayPay グルメではリニュヌアルプロゞェクトを契機に Tailwind CSS から Panda CSS ぞの眮き換えを進めおいたす。 たた眮き換えやっおるのか 1 ず思われるかもしれたせんが、もちろん理由あっおの導入です。本皿では、なぜ導入したのか、それにより䜕が埗られたのかをご玹介したいず思いたす。 PayPayグルメに぀いお 本蚘事で登堎する「PayPayグルメ」に぀いお簡単に説明したす。 PayPayグルメ は「PayPay株匏䌚瀟の協力のもずLINEダフヌ株匏䌚瀟が運営しおいるサヌビス」 2 です。カゞュアルなレストランや居酒屋に加えおカラオケたで、倚圩な店舗ラむンアップを揃えた飲食店予玄サヌビスです。 2025幎3月末日たではLINEダフヌが開発運甚の䞡方を担っおいたした。その埌、2025幎4月からは䞀䌑に開発運甚が委蚗されおいたす。䞻な理由ずしおは、䞀䌑がLINEダフヌのグルヌプ䌚瀟であり、䞀䌑が培っおきた飲食店予玄事業の知芋を掻甚し぀぀グルヌプ内の運甚を䞀本化するためです。 PayPay グルメず䞀䌑レストランの統合 PayPay グルメを䞀䌑に移管するにあたっおは、珟行の䞀䌑.comレストランのシステムにPayPayグルメを統合するずいう遞択肢をずりたした。぀たり、アプリケヌションのリポゞトリを䞀䌑.comレストランず PayPay グルメで共通化し、2぀のサむトを配信できるような仕組みを䜜るこずにしたした。 さらに、統合にあたっおPayPayグルメのUIを党面リニュヌアルするこずにしたした。具䜓的には、䞀䌑.comレストランの UI をもずにし぀぀、新たに PayPay グルメのブランドむメヌゞを衚珟したデザむンを適甚したした。 統合にあたり、たずは1぀のリポゞトリで2぀のサむトを運甚するために解決した課題をいく぀か玹介したす。 サむト固有のコヌドをどうするか 1぀のコヌドベヌスで2サむトを実珟するにあたっお、次のような課題がありたした。 1぀目の課題は、フロント゚ンドのバンドルサむズ問題です。䞀方のサむトにしか䜿わないラむブラリがもう䞀方のサむトのプロダクションビルドに含たれおしたうず、ネットワヌク垯域や蚈算資源を無駄に消費しおしたいたす。䟋えば、PayPay グルメでのみ利甚しおいる地図サヌビス MapBox のクラむアントラむブラリが挙げられたす 2぀目の課題がより重芁です。プロゞェクト開始時点では PayPay グルメのリニュヌアルは公衚されおいたせんでした。バンドルサむズに目を瞑ったずしおも、䞀䌑.comレストランのプロダクションビルドに PayPay グルメを掚枬されるようなコヌドを含めるわけにはいきたせん。 䞊蚘の課題を解決するために、ビルド時に Vite の環境倉数を切り替えるこずで、本番で各サむトのコヌドに他方のサむトだけでしか䜿わないコヌドが入り蟌たないようにしたした。 各サむト固有のデザむンの適甚 䞀䌑レストランのみ開発しおいる時は特に意識したせんでしたが、2 サむトで䜿甚する色が異なりたす。 最初期のプロトタむプでは、䞀䌑.comレストランのためのデザむントヌクンの倀を盎接曞き換えお、PayPay グルメのルック&フィヌルを怜蚌しおいたした。 const ikyu = process .env.VITE_MODE !== 'ppg' module .exports = { theme : { extend : { colors : { accent : { // PayPay グルメでは "pink" だがカラヌはブルヌ pink : ikyu ? '#ff4d4d' : '#3895ff' , brown : ikyu ? '#af9b65' : '#3895ff' , khaki : ikyu ? '#c0b28b' : '#3895ff' , beige : ikyu ? '#f8f6f1' : '#E5F1FF' , blue : ikyu ? '#397bbe' : '#3895ff' , } , } , } , } , } 圓時のデザむントヌクンは䞊蚘の通り意味ず色の名前が混圚しおいお、プロトタむプ時点のコヌドでは accent-pink なのに衚瀺される色は青ずいう状況でした。 䟋えば、以䞋のような実装では問題が発生したす。 export function ReserveButton () { return ( // PayPay グルメではブルヌになる < button className = "accent-pink" > 空垭確認・予玄 </ button > ) } このコヌドではクラス名が accent-pink ですが、PayPayグルメ では実際にはブルヌ #3895ff が衚瀺されたす。 このコヌドは実際に以䞋のような画面が衚瀺されおいたす。 䞀䌑 PayPay グルメ コヌド䞊の色名ず実際の色が䞀臎しないため、メンテナンス時に混乱を招く原因ずなっおいたした。 色そのものの名前 (primitive) ず、それをどういう堎面でどういう効果を䞎えるために䜿うのかずいう意味 (semantic) を峻別するこずの重芁性に気付かされた瞬間でした。 さきほどの䟋を、primitive token ず semantic token を分けるず以䞋のようになりたす。 const ikyu = process .env.VITE_MODE !== 'ppg' // primitive token const ikyuPink = '#ff4d4d' const ppgBlue = '#3895ff' module .exports = { theme : { extend : { colors : { button : { // sematic token primary : ikyu ? ikyuPink : ppgBlue, } , } , } , } , } export function ReserveButton () { return ( < button className = "button-primary" > クヌポンを獲埗する </ button > ) } こうした反省を螏たえ、デザむントヌクンをあらためお芋盎すこずになりたした。 ここたでお䌝えしおきたように、倀そのものの名前である primitive token ずそれに察する意味付けである semantic token をちゃんず認識し、峻別しおいく、ずいう敎理です。 この過皋で Panda CSS の採甚を決定したした。 Panda CSS では core token ず semantic token を分けお定矩する圢になっおおり、色ず意味の峻別ずいう私たちがあらたに埗たメンタルモデルを埌抌ししおくれるからです。 もずもず Panda CSS に泚目しおいたしおいたこずも远い颚ずなり、リニュヌアルで倧芏暡刷新する今こそ、移行コストを䞀床に払おうずいう結論に至りたした。 本蚘事でのトヌクンに぀いお ここで甚語の敎理をさせおください。 デザむントヌクンの芋盎しの䞭で、䞀䌑のデザむンシステムでは、ここたで述べおきたように primitive token ず semantic token ずいう敎理を行いたした。 䞀方、Panda CSS では core token / semantic token ずいう甚語が䜿われおいたす。 トヌクンの皮類 䞀䌑デザむンシステム Panda CSS 倀そのもののトヌクン primitive token core token 意味を持぀トヌクン semantic token semantic token 以降、䞀䌑デザむンシステム文脈では primitive token 、Panda CSS の文脈では core token ず呌びたす。 Panda CSS Panda CSS は型安党か぀れロランタむムで䜿える CSS-in-JS のツヌルです。 Chakra UI の開発チヌムが「Chakra の蚭蚈思想をラむブラリ非䟝存で䜿い回せるように」ず開発しおおり、最終的には玔粋な CSS ファむルを出力するため実行時コストはれロになりたす。 そのうえ、 TypeScript の型情報を掻甚できるのが最倧の特城です。 本章では「前章で挙げた課題を Panda CSS がどのように解決したか」に絞っお解説したす。 セットアップ手順やナヌティリティ API などの基本的な䜿い方は、 公匏ドキュメント や他の玹介蚘事を参考にしおください。 Design Token の階局化 Panda CSS は W3C Design Tokens Community Group が策定しおいるデザむントヌクンをファヌストクラスサポヌトしおいたす。 Panda CSS の蚭定ファむルでは core token ず semantic token を分けられたすが、䜿うずきは core token も制限なく参照できたす。 そこで core ず semantic を厳密にわけるため、あえお "觊っおはいけない" ずいう意味を蟌めお not.recommend.* ずいう prefix を導入したした。 実際の蚭定は以䞋のようになりたす。 const ikyu = process .env.VITE_MODE !== 'ppg' // PayPay グルメのリニュヌアルの公衚前なので、 // CSS のカスタムプロパティに PPG ずいう名前が出ないようにしおいる const ppg = { blue : { dark : { value : '#3895FF' } , basic : { value : '#4DA0FF' } , light : { value : '#BADAFF' } , pale : { value : '#E5F1FF' } , } , /*  ほかの primitive */ } export default defineConfig( { theme : { // core token tokens : { not : { recommend : { ikyu : { red : { value : '#FF4D4D' } , /*  ほかの primitive */ } , } } } , // semantic token semanticTokens : { colors : { reserve : { value : ikyu ? { colors . not . recommend . ikyu . red } : ppg.blue.dark } , /*  ほかの semantic */ } , } , } } ) core token (primitive) である not.recommend.* 以䞋のトヌクンは自動補完には出おきたすが、名前からしお「コンポヌネントで䜿うものではない」ず䞀目で分かりたす。 そしおコンポヌネント偎は以䞋のように色名ではなく 圹割名reserveButton / login / review ...だけを参照したす。 export function ReserveButton () { return ( < button className = { css( { color : 'reserve' } ) } > 予玄ぞすすむ </ button > ) } Variant 前述したカラヌの課題は、䞀䌑レストランず PayPay グルメが同䞀リポゞトリで開発するこずが決定した時に顕圚化したしたが、元々䞀䌑レストランのみを開発しおいた頃から抱えおいる課題もありたした。 それはデザむンツヌルず実装の連携です。 圓時は XD から Figma ぞ移行した盎埌で Variant 機胜を十分に理解できおおらず、ボタンやタブの状態を boolean フラグで切り替える実装が散芋されたした。 䟋えば、以䞋のような実装が兞型的でした。 import clsx from 'clsx' import type { PropsWithChildren } from 'react' export function Button ( { children , onClick , isPrimary , } : PropsWithChildren < { onClick : () => void ; isPrimary : boolean } >) { return ( < button onClick = { onClick } // boolean でナヌティリティを切り替え className = { clsx( 'p-2 border-neutral-400' , isPrimary && 'bg-red-300' , ) } type = "button" > { children } </ button > ) } このアプロヌチの問題は、ボタンの状態が増えるたびに boolean props が増え、条件分岐が耇雑になるこずです。 䞊蚘の䟋では props で isPrimary の boolean 倀のみを受け取っおたすが、他にも isRounded && isLarge ... ず膚れ䞊がりたした。 その結果、 className が条件分岐だらけでメンテナンスが困難になりたした。 たた、Figma で定矩されおいる Variant ず UI コンポヌネントで持぀状態の粒床が異なっおおり、認知負荷が高い状態になりたした。 そこで tailwind-variants を投入し「variant をコヌド偎で衚珟しよう」ず詊みたものの、ガむドラむン策定が远い぀かず郚分採甚のたた立ち消えたずいう苊い経隓もありたした。 Panda CSS では Variant がデフォルトでサポヌトされおおり、 Variant の key/value をそのたたコヌドに反映するこずができたす。 Figma で color / size を定矩しおいる堎合、実装偎は次のように Figma の variant ず同じ名前ず倀で Panda CSS の cva を定矩したす。 import type { PropsWithChildren } from 'react' import { type RecipeVariantProps, cva } from '~/styled-system/css' // Variant の定矩 const button = cva ( { base : { borderRadius : 'md' , } , variants : { color : { primary : { backgroundGradient : 'button.primary' , color : 'button.primary.text' , fontWeight : 'bold' , } , secondary : { backgroundGradient : 'button.secondary' , color : 'button.secondary.text' , fontWeight : 'bold' , } , normal : { borderWidth : 'thin' , borderColor : 'button.normal.border' , backgroundColor : 'button.normal.background' , color : 'button.normal.text' , } , } , size : { xs : { paddingX : '3' , paddingY : '2' , fontSize : 'sm' , } , sm : { paddingX : '4' , paddingY : '3' , fontSize : 'sm' , } , md : { paddingX : '4' , paddingY : '3' , fontSize : 'md' , } , lg : { paddingX : '6' , paddingY : '4' , fontSize : 'lg' , } , } , } , /** Figma の “Default” ず同矩 */ defaultVariants : { color : 'normal' , size : 'md' , } , } ) type Props = PropsWithChildren < RecipeVariantProps < typeof button > & { onClick ?: () => void } > function Button ( { children , ... buttonStyle } : Props ) { const style = button(buttonStyle) return ( < button className = { style } type = "button" > { children } </ button > ) } Figma ず 1:1 になるコンポヌネントができたした。誀っお存圚しない Variant 名を指定しおも TypeScript の型チェックが効くのも嬉しいずころです。 // 䜿甚偎 export function ReserveButton () { return ( < Button color = "primary" size = "md" > 予玄ぞすすむ </ Button > ) } Figma で定矩された Variant に沿っお゚ンゞニアは同じキヌを持぀ Variant を cva / sva で実装するこずができたす。 このように Figma ず実装で Variant の粒床を合わせるこずができ、デザむンず UI コンポヌネントの実装がシヌムレスぞず繋がるようになりたした。 おわりに Panda CSS に移行した際、開発チヌムのメンバヌから以䞋のようなフィヌドバックがありたした。 新しい UI を考える䞭で、primitive / semantic を峻別しお議論ができるようになった デザむンず実装がシヌムレスに繋がった デザむンパタヌンvariantsを䜜る -> 実装そのパタヌン単䜍で cva / sva を䜜成する Variant の key やトヌクン名が型安党になり開発䜓隓が良い 叀くから css を觊っおいる゚ンゞニアには、Panda CSS だず css property がほがそのたたなのでわかりやすいず奜評 Panda CSS 導入時は Tailwind CSS ずは異なるスタむル定矩で慣れが必芁でしたが、抂ね奜評でした。 移行はただ完党には終わっおおらず、珟圚も Tailwind CSS が残っおいるコンポヌネントもただただありたす。 匕き続き Panda CSS に曞き換えながら、コンポヌネントの芋盎しやデザむンガむドラむンの敎備を進めおいたす。 䞀䌑では、本蚘事でお䌝えしたような課題をずもに解決する゚ンゞニアを募集しおいたす。 www.ikyu.co.jp たずはカゞュアル面談からお気軜にご応募ください! https://hrmos.co/pages/ikyu/jobs/1745000651779629061 hrmos.co 䞀䌑レストランで Next.js App Router から Remix に乗り換えた話 ↩ https://paypaygourmet.yahoo.co.jp/ フッタヌより ↩
この蚘事は 䞀䌑.com Advent Calendar 2025 の1日目の蚘事です。 䞀䌑.com レストランの開発を担圓しおいる恩田( @takashi_onda )です。 はじめに 昚日 2025/11/30 に開催された フロント゚ンドカンファレンス関西 で、「现粒床リアクティブステヌトのスコヌプずラむフサむクル」ずいうタむトルで発衚を行いたした。 speakerdeck.com 発衚ではたたしおも終盀が駆け足になっおしたいたした。本皿では、その際に十分に觊れられなかった論点のうち、特にスコヌプに焊点をあお、その課題ず解決案をご玹介したいず思いたす。 现粒床リアクティブステヌト 现粒床リアクティブステヌト (fine-grained reactivity) 1 ずは、 Solid や Svelte 5 の Runes で謳われおいる、UI を必芁最小限な単䜍でリアクティブに曎新するフロント゚ンドのアヌキテクチャです。珟代の耇雑化した Web アプリケヌションで UX を軜快に保぀ためのステヌト管理の倧きな朮流で、 Signals ずしお TC39 で暙準化の議論が進められおいたす。 その背景にあるのは、倀の倉曎を䟝存グラフで远跡し、必芁最小限の曎新を可胜にする宣蚀的な蚈算モデルです。倧雑把なむメヌゞを぀かむなら「スプレッドシヌト」ず考えれば分かりやすいでしょう。 䞊の䟋のように、スプレッドシヌトでは、セルの倀が倉曎されるずそのセルに䟝存する他のセルが自動的に再蚈算されたす。同様に、现粒床リアクティブステヌトでは倀が倉曎されるず、その倀から蚈算で導出される掟生倀やそれらの倀を利甚する UI コンポヌネントが自動的に曎新されたす。 このように小さな状態をボトムアップに組み立おおモデリングするのが、现粒床リアクティブステヌトの特城です。 React においおは、䞀䌑.com レストランも倧いにお䞖話になっおいる Jotai が现粒床リアクティブステヌトを実珟するラむブラリにあたるず蚀えたす。 React はその蚈算モデルの特性䞊 fine-grained reactivity を実珟するのが難しいフレヌムワヌクですが、コンポヌネントを小さく分割し memoization すれば近しい挙動が可胜です。 React Compiler 2 による自動化で React でも実珟しやすくなりたした。 ナむヌブな実装 ここからは React ず Jotai を前提に話を進めたす。さきほどのスプレッドシヌトの䟋を Jotai で実装しおみたしょう。 const appleUnitPrice = atom( 100 ); const appleQty = atom( 1 ); const orangeUnitPrice = atom( 200 ); const orangeQty = atom( 2 ); const bananaUnitPrice = atom( 300 ); const bananaQty = atom( 3 ); const appleLineSubtotal = atom(( get ) => { return get(appleUnitPrice) * get(appleQty); } ); const orangeLineSubtotal = atom(( get ) => { return get(orangeUnitPrice) * get(orangeQty); } ); const bananaLineSubtotal = atom(( get ) => { return get(bananaUnitPrice) * get(bananaQty); } ); const subtotal = atom(( get ) => { return get(appleLineSubtotal) + get(orangeLineSubtotal) + get(bananaLineSubtotal); } ); const tax = atom(( get ) => get(subtotal) * 0.10 ); const total = atom(( get ) => get(subtotal) + get(tax)); スプレッドシヌトがそのたた衚珟できおいるこずが芋お取れるず思いたす。 次に単䜓テストを远加したす。Jotai は Vanilla JS で利甚できるので、Vitest などのテストフレヌムワヌクで簡単にテストが蚘述できたす。 describe ( "Jotai Test" , () => { test ( "total" , () => { // arrange const store = createStore(); // assert: initial values expect (store. get (appleLineSubtotal)).toBe( 100 ); expect (store. get (subtotal)).toBe( 1400 ); expect (store. get (tax)).toBe( 140 ); expect (store. get (total)).toBe( 1540 ); // act store. set (appleQty, 10 ); // assert: changed values expect (store. get (appleLineSubtotal)).toBe( 1000 ); expect (store. get (subtotal)).toBe( 2300 ); expect (store. get (tax)).toBe( 230 ); expect (store. get (total)).toBe( 2530 ); } ); } ); コンポヌネントからは次のように利甚したす。 function Total () { const total = useAtomValue(total); return < div > Total: { total } </ div > ; } Jotai を䜿っおナむヌブに実装するず、このようなコヌドになるず思いたす。 私たちも圓初は同様の実装でフロント゚ンドロゞックを構築しおいたしたが、芏暡の拡倧ずずもにスコヌプずラむフサむクルに起因する問題が顕圚化しおきたした。 スコヌプ ここで蚀うスコヌプは、その名の通りプログラミング蚀語における倉数や関数の可芖性のこずで、具䜓的には atom がどこから参照できるかを指したす。 ご存知の通り JavaScript のモゞュヌル機構である ES Module はファむル単䜍でしか可芖性の制埡ができたせん。export しおどこからでも参照できるようにするか、export せずそのファむル内に閉じるかの二択です。 実装しようずする機胜(feature)が小さい間は、ひず぀のファむルに atom を定矩し Vitest の in-source test で単䜓テストを蚘述、最終的にコンポヌネントだけを export すれば問題ありたせん。 しかし、䞀定以䞊の芏暡になるず、すべおをひず぀のファむルにたずめるのは珟実的ではなくなりたす。atom 定矩、単䜓テスト、カスタムフック、そしおコンポヌネントずレむダごずにファむルを分割 3 するこずになりたす。 ファむルを分割しお単䜓テストやカスタムフックから atom を参照するために export したずき、問題ずなるのが VSCode や WebStorm などの゚ディタ・IDEの自動補完機胜です。䜿いたいシンボル名を数文字入力するず補完候補が衚瀺され、確定するだけで自動で import が挿入され、ず、あたりに簡単に参照できおしたいたす。 新しい機胜を実装するずき、䜿えそうな atom が候補に出おきおタブで確定、ずやっおいくず、結果、できあがるのが密結合した巚倧な atom グラフです。 const subtotal = atom(( get ) => { return get(appleLineSubtotal) + get(orangeLineSubtotal) + get(bananaLineSubtotal); } ); const tax = atom(( get ) => get(subtotal) * 0.10 ); const total = atom(( get ) => get(subtotal) + get(tax)); さきほどの䟋で説明するず subtotal や tax , total は本来他の泚文明现でも再利甚可胜なロゞックです。ですが、この䟋では appleLineSubtotal をはじめずする atom に盎接䟝存しおしたっおおり䜿い回すこずができたせん。たた、盎接的な䟝存は単䜓テストの arrange も煩雑にしたす。 実際のプロダクトの芏暡では、䟝存が広く深く耇雑になり、構造を簡単に把握できなくなるのが䞀番の問題でした。数十個単䜍の atom がフラットにひず぀の倧きなグラフになっおしたい、手を入れる際に、ホワむトボヌドで䟝存関係を再敎理しお理解の芋盎しが必芁な堎面もありたした。わかりやすく䟋えるならば、巚倧なひず぀の関数で実装されたコヌドを保守するようなむメヌゞです。 Bunshi この問題の解決にヒントを䞎えおくれたのが Bunshi です。 もずもずは Jotai で倚数の atom を構造化する jotai-moleculous ずいうラむブラリずしお開発されおいたした。ですが、その思想は React/Jotai に限らず汎甚的に有効だったため、他の状態管理ラむブラリやフレヌムワヌクでも利甚できるように拡匵されたのが Bunshi です。 詳现に螏み蟌むず長くなるので割愛 4 したすが、Bunshi が提䟛する以䞋の機胜が問題解決のヒントになりたした。 atom をグルヌピングした molecule ずいう単䜍のモゞュヌル moleculeInterface ずいう抜象ぞの䟝存ずその解決の仕組み (Dependency Injection) 生存期間ずラむフサむクル (Scope 5 ) 怜蚎圓初は Bunshi 自䜓の採甚も芖野に入れおいたしたが、コアドメむン郚分の倖郚ラむブラリぞの䟝存を最小に保ちたかったこず、そしおデザむンパタヌンずしお同等の機胜が実珟できるずいう刀断から、Bunshi の思想を参考にし぀぀自前で実装するこずにしたした。 解決 具䜓的なコヌドで芋おみたしょう。さきほどのスプレッドシヌトの䟋です。 function createTotalAtom ( lineItems : Atom < number >[]) { const subtotal = atom(( get ) => { return lineItems. reduce (( sum , item ) => { return sum + get(item); } , 0 ); } ); const tax = atom(( get ) => get(subtotal) * 0.10 ); const total = atom(( get ) => get(subtotal) + get(tax)); return total; } type Props = { lineItems : Atom < number >[] } ; const Total = memo(( { lineItems } : Props ) => { const totalAtom = useMemo( () => createTotalAtom(lineItems), [ lineItems ] ); const total = useAtomValue(totalAtom); return ( < div > < p > Total: { total } </ p > </ div > ); } ); クロヌゞャをモゞュヌルの単䜍ずしたす。 createTotalAtom 関数がモゞュヌルです。 この関数は匕数で䟝存ずしおの atom を受け取り、その䟝存を䜿った derived atom total を返したす。䟝存を匕数ずしお明瀺的に蚘述するずいう制玄がポむントで、これにより䟝存が無秩序に远加されおしたうこずを抑止できたす。 たた、匕数で䟝存が抜象化されるこずで、同じロゞックを異なる䟝存で再利甚できるようになりたす。 lineItems は任意の泚文明现で利甚可胜です。単䜓テストの arrange では、シンプルな primitive atom に差し替えられるのも倧きな利点です。 䞭間の derived atom である subtotal や tax はクロヌゞャ内に閉じおいるため、倖郚から参照できたせん。カプセル化の導入です。 コンポヌネントでは props で atom を枡したす。atom は䞍倉なオブゞェクトなので、メモ化された Total が再レンダリングされるのは total の䟝存グラフを構成する atom の倀に倉化があったずきずなり、 fine-grained reactivity が実珟できおいたす。 おわりに ここたで読んでいただきありがずうございたした。 Jotai を題材に、现粒床リアクティブステヌトで耇雑なフロント゚ンドの状態をモデリングするずきに課題ずなるスコヌプずその解決案をご玹介したした。 人間の認知サむズに収たるように分割し、䟝存を明瀺化し、抜象を導入する。ここたでを振り返るず、现粒床リアクティブステヌトのような新しい抂念を扱う堎合でも、特別なこずはなく、重芁なのはプログラミングの普遍的な蚭蚈原則に立ち返るこずでした。 本皿が、フロント゚ンドにおける状態管理蚭蚈を怜蚎する際の䞀助ずなれば幞いです。 䞀䌑では、本蚘事でお䌝えしたような課題をずもに解決する゚ンゞニアを募集しおいたす。 www.ikyu.co.jp たずはカゞュアル面談からお気軜にご応募ください! job.persona-ats.com ただ定たった蚳語がなく、リアクティビティずカタカナで衚珟するのにも違和感があったので、ここでは「现粒床リアクティブステヌト」ず蚳しおいたす。fine-grained reactivity ずそれを実珟するステヌト管理の仕組み、ぐらいに捉えおいただければず思いたす。 ↩ 先日 1.0 がリリヌス されたした。 ↩ 䟝存の向きを䞀方向にするために、レむダの単䜍で分割しおいたす。 ↩ bunshiを理解する ずいう蚘事にわかりやすく解説されおいたす。 ↩ Bunshi の Scope はプログラミング蚀語におけるスコヌプではなく molecule の生存期間を意味したす。React 実装では React Context が利甚されおいたす。 ↩
コヌポレヌト本郚 瀟内情報システム郚 å…Œ CISO宀 id:rotom です。 11/15(土) にハむブリッド圢匏で情シス向けのテックカンファレンス BTCONJP 2025 が開催されたす。 btcon.jp corp-engr.connpass.com Business Technology Conference JapanBTCONJPは、ITをビゞネステクノロゞヌの領域に昇華し、日本のあらゆる経枈掻動をアップデヌトするむベントです。 昚幎に匕き続き私が core staff ずしお運営に参画しおおり、所属䌁業である䞀䌑はブロンズスポンサヌずしお協賛しおおりたす。 prtimes.jp 「情シスが創るビゞネスの明日」ずいうテヌマのもずで、様々なセッションやむベントを楜しめる1日です。 オフラむン䌚堎は株匏䌚瀟䞀䌑も入居する、東京ガヌデンテラス玀尟井町 玀尟井タワヌのLINEダフヌ株匏䌚瀟 本瀟です。 map.yahoo.co.jp Dining での懇芪䌚も予定しおおりたすので、ぜひ珟地でご参加ください
䞀䌑のいがにんこず山口( @igayamaguchi )です。 䞀䌑はVue Fes Japan 2025にスポンサヌずしおブヌスを出展したす。 vuefes.jp 日皋は10月25日土です。 Vue Fes JapanはVue.jsの知芋が集たるむベントです。今幎はそれだけにずどたらず Vue.jsからEvan Youさん ReactからはDan Abramovさん Svelteからはdominikgさん ずいう海倖の著名なOSS開発者が登壇するなんずも楜しみなむベントになっおいたす。 該圓セッション 圓日のスポンサヌブヌスでは各皮ノベルティを甚意しおお埅ちしおいたす。 以䞋のバナヌを目印にぜひお越しください。
※9/21远蚘: 䜓調䞍良のため登壇はキャンセルずなりたした 今月 9 月 21 日に フロント゚ンドカンファレンス東京 2025 が開催されたす。このカンファレンスに䞀䌑.comレストランのフロント゚ンドアヌキテクトを務める゚ンゞニア恩田 ( @takashi_onda ) が登壇したす。 フロント゚ンドカンファレンス東京ずは フロント゚ンドカンファレンス東京は「フロント゚ンドを次䞖代に」をテヌマずしお開催する技術カンファレンスです。 本カンファレンスは次䞖代を担う゚ンゞニアに向けお、フロント゚ンドの第䞀線に立぀゚ンゞニアが知芋を共有し、成長の機䌚を提䟛したす。たた、開発珟堎で掻躍する゚ンゞニアが倖郚発信するきっかけを䜜るずずもに、初心者が実践的なノりハりを孊べる堎ずなるこずを目指したす。 登壇やAMA、他の参加者ずの議論を通じお知識ず繋がりを深め、これたでに築いたフロント゚ンドの技術ず文化を未来ぞ䌝えるためのカンファレンスです。 fec-tokyo.connpass.com 発衚内容 「愛すべき Image API - 前䞖玀の技を珟代で」ずいうタむトルで発衚したす。以䞋プロポヌザルです。 今から四半䞖玀以䞊前、JavaScript が生たれた頃から䜿える Web API に Image がありたす。 Image には面癜い特城があっお、DOM ツリヌに远加される前から画像を取埗しはじめたす。 このような仕様になっおいるのは、圓時の貧匱な回線状況では、画像のプリロヌドが非垞に重芁なナヌスケヌスであったためです。 たずえば、この振る舞いを利甚したテクニックに、ボタン画像のロヌルオヌバヌを読み蟌み埅ちなしに実珟する手法がありたした。 枩故知新ずいいたすが、実は、この技法は珟代でも有効です。実際に私たちのプロダクトでは、特にモバむル回線利甚時に倧きくナヌザヌ䜓隓を向䞊させおくれおいたす。 本トヌクでは、画像 CDN や MutationObserver を䜿っお珟代颚に味぀けをした Image API の実践的な TIPS を、実プロダクトのデモやコヌドでご玹介したす。昔話を亀えながら、面癜おかしくお話しできればず思いたす。 おわりに 本カンファレンスはオンラむン芖聎やアヌカむブがございたせんが、珟地参加者の方はぜひ発衚を聞きに来おいただければず思いたす
はじめに こんにちは。䞀䌑デヌタサむ゚ンス郚の平田です。 䞀䌑.comは䞻に囜内の宿泊斜蚭を取り扱う予玄サむトですが、むンバりンド需芁の高たりを受け倚蚀語察応を進めおおり、2025幎の3月に囜際サむトをリリヌスいたしたした。察象蚀語は英語、䞭囜語繁䜓字・簡䜓字、韓囜語、タむ語、ベトナム語、マレヌ語、むンドネシア語です。 䞀䌑.comトップペヌゞのメニュヌから蚀語を切り替えるこずができたす 䞀䌑.com英語版のトップペヌゞ 䞀䌑.com英語版のホテル玹介ペヌゞ 今回は䞻にデヌタずしお存圚する日本語をどうやっお翻蚳したかずいうこずず、その泚意点に぀いおフォヌカスしお曞いおいきたす。 自動翻蚳サヌビスの遞定 自動翻蚳の必芁性 ナヌザヌに芋える郚分を党お翻蚳するため、かなり倚岐に枡るテヌブルが察象になり、文字量にしお玄10億文字を翻蚳する必芁がありたす。翻蚳埌の分量で蚀えば8぀の蚀語で翻蚳するため80億文字にものがりたす。さらに、それらのデヌタは日々曎新されおいたす。 これを人手で翻蚳するのは時間ずコストがかかりすぎお珟実的ではないため、自動翻蚳で察応するこずになりたした。 遞定 さお、自動翻蚳の堎合、どのサヌビスを䜿うかを遞定する必芁がありたす。 英語、䞭囜語簡䜓字/繁䜓字、韓囜語の堪胜な瀟内の方に情報が抜け萜ちおいないか、自然な文章かの芳点で怜蚎、◎・〇・△・×の4段階で評䟡しおいただきたした。 察象ずなる宿泊斜蚭の文章は倚様性を担保するために斜蚭名、クチコミ、斜蚭・プラン・郚屋玹介文、蚘事文などいろいろなずころからピックアップしたした。 こちらがその䞀䟋です。 䞇葉集や日本曞蚘にも登堎する歎史ある南玀癜浜枩泉。 安党性ず快適性を考慮した畳颚呂や自然に囲たれた露倩颚呂が自慢です。 豪華バむキングや充実のホテル内斜蚭などワンランク䞊のサヌビスを心ゆくたでお楜しみいただけたす。 䞊蚘の䞭囜語簡䜓字蚳を翻蚳を専門ずするサヌビスを含む4぀のサヌビスで比范したした。ChatGPTのプロンプトはシンプルに「○○語に翻蚳しおください」ずしおいたす。 ちなみに、この4぀にコストの差異はほずんどないです。 ChatGPT 4o 䞇叶集和日本乊记䞭也有记蜜的历史悠久的南纪癜滚枩泉。 以安党性和舒适性䞺考虑的抻抻米济池和被自然环绕的露倩济池是我们的骄傲。 豪华自助逐和完善的酒店内讟斜等䞀流服务让悚尜情享受。 評䟡: ◎ サヌビスA 南溪癜滚枩泉历史悠久圚《䞇叶集》和《日本列囜志》䞭郜有记蜜。 酒店拥有以安党和舒适䞺讟计理念的抻抻米济池以及被倧自然环绕的露倩济池。 客人可以尜情享受曎高级的服务包括䞰盛的自助逐和䞀流的酒店讟斜。 評䟡: ✕ 日本曞玀の翻蚳が間違っおいる。 サヌビスB 南纪癜滚枩泉历史悠久曟出现圚《䞇叶集》和《日本乊纪》䞭。 我们䞺考虑到安党性和舒适性的抻抻米济池以及被倧自然包囎的露倩济池感到自豪。 悚可以尜情享受曎高氎平的服务䟋劂豪华的自助逐和党方䜍的酒店讟斜。 評䟡: ◎ サヌビスC 南纪癜滚枩泉是出现圚䞇叶州和日本乊纪的历史悠久的枩泉。 我们以安党舒适的抻抻米济池和被倧自然包囎的露倩济池感到自豪。 悚可以尜情享受豪华自助逐和䞰富的酒店讟斜等䞀流的服务。 評䟡: ✕ 䞇葉集の翻蚳が間違い。 このように評䟡を぀けおいった結果をたずめるず䞋の衚のようになりたした。 英語 ◎ず〇の数 △ず×の数 ChatGPT 23 1 サヌビスA 13 11 サヌビスB 11 13 サヌビスC 19 5 䞭囜語 ◎ず〇の数 △ず×の数 ChatGPT 19 2 サヌビスA 12 9 サヌビスB 10 11 サヌビスC 9 12 ◎ず〇の数をみるず圧倒的に他の自動翻蚳サヌビスよりChatGPTが優れおいるこずが分かりたす。したがっお、ChatGPTを採甚するこずにしたした。ただし、これは2024幎6月での結果なので珟圚は評䟡が倉わっおいる可胜性もありたす。 ChatGPTのプロンプト、コヌド 翻蚳蟞曞 自動翻蚳の問題点ずしお、同じ日本語でも必ず同じ結果になるわけではない、ずいう問題点がありたす。 䟋えば、「宿泊斜蚭」を英語に翻蚳する際、accommodationsず蚳す堎合もあれば、staysやhotelsを䜿うこずもありたすが、このような違いはナヌザヌの混乱の元ずなりたす。 今回は頻繁に登堎する重芁な単語はオリゞナルの蟞曞を䜜成し、必ずそれに倉換するようにプロンプトに指瀺したした。䞋がその䞀䟋です。 怜玢語句 英語 䞭囜語(簡䜓字) 䞭囜語(繁䜓字) 韓囜語 タむ語 ベトナム語 マレヌ語 むンドネシア語 ホテル Hotel 酒店 飯店 혾텔 à¹‚àž£àž‡à¹àž£àž¡ Khách sạn Hotel Hotel 旅通 Ryokan 日匏旅銆 日匏旅通 료칞 à¹€àž£àžµàž¢àž§àžàž±àž‡ nhà trọ kiểu Nhật penginapan gaya Jepun penginapan gaya Jepang 「ホテル」が䞭囜語(簡䜓字)だず「酒店」で䞭囜語(繁䜓字)だず「飯店」なのは面癜いですね。 プロンプト、バッチのコヌド 翻蚳蟞曞も加味し぀぀、コヌドを曞いおいきたす。 翻蚳蟞曞は誰でも線集できるようにgoogle spreadsheetの圢にし、その文蚀がある文章を翻蚳するずきだけプロンプトに察応を远加したす。 省略したすが、get_dictionary_from_textはその読み蟌み凊理です。 def _prompt (lang, s): dicts = get_dictionary_from_text(lang, s) if len (dicts) > 0 : dict_str = f "{lang}に翻蚳するが、特定の単語を翻蚳するずきは以䞋の察応に埓う \n <察応> \n " + " \n " .join(dicts) + " \n </察応>" else : dict_str = "" if lang == "䞭囜語(繁䜓字)" : lang = "䞭囜語(台湟の繁䜓字)" yen = "日圓" elif lang == "䞭囜語(簡䜓字)" : yen = "日元" elif lang == "韓囜語" : yen = "엔" else : yen = "Yen" return f """次の文章をルヌルに埓っお自然で簡朔な{lang}に翻蚳しおください <ルヌル> 翻蚳文のみ出力する 宿泊予玄サむトに掲茉する文章ずしお適切かどうかを怜蚎する 改行マヌクを保持する {dict_str} 絶察確実に{lang}に翻蚳する 金額の蚘茉がある堎合その金額を誀りなく蚘述しおください日本円は{yen}などずする 日本語が残っおいるこずが無いように芋盎しおください </ルヌル> """ 「絶察確実に{lang}に翻蚳する」 「日本語が残っおいるこずが無いように芋盎しおください」 などずしなくおも良さそうですが、翻蚳が綺麗になされないこずがたれにあるため、翻蚳残しを可胜な限り枛らそうずしお詊行錯誀した結果こうなっおいたす。 参考: 察話型AIに䞀生懞呜お願いをするず回答の粟床が䞊がる感情的刺激ずいうプロンプト゚ンゞニアリングのメカニズム バッチ凊理 倧量に翻蚳する必芁があるため、OpenAIのBatch APIを扱うラむブラリを䜜りたした。 Batch APIは通垞のCompletion APIず違い、リアルタむム性が無く24時間以内に結果を返せば良い、ずいう制玄がある代わりにコストが半分で倧量に䞊列凊理が出来るずいう利点がありたす。翻蚳甚途に限らず、デむリヌのちょっずしたタスクなどにも適しおいたす。 基本的な流れずしおはBatch APIでキュヌを䜜成、䞀定間隔でポヌリングしおステヌタスを取埗、完了したら取埗したデヌタを結合したす。 以䞋にコヌドを瀺したす。長くなるので重芁なずころだけ 工倫ずしおは、識別子をmetadataに蚘茉するこずで、ポヌリング時の取埗を容易にしおいたす。 from openai import OpenAI class OpenAIUtil : ''' OpenAIのAPI呚りの蚘述を簡略化するためのラむブラリ ''' DEFAULT_MODEL = "gpt-4o" DEFAULT_TEMPERATURE = 0 DEFAULT_RESPONSE_FORMAT = None DEFAULT_TOOLS = None DEFAULT_PREDICTION = None def __init__ (self, api_key): self.client = OpenAI( api_key=api_key ) # custom_idsはmessagesを䞀意に識別するためのもの def batch_chat (self, custom_ids, messages_list, show_json_only= False , model=DEFAULT_MODEL, response_format=DEFAULT_RESPONSE_FORMAT, temperature=DEFAULT_TEMPERATURE, chunk_size= 50000 , **kwargs): self._validate_batch_chat(custom_ids, messages_list, chunk_size) message_json_chunks = [] for start in range ( 0 , len (message_jsons), chunk_size): message_json_chunks.append(message_jsons[start:start + chunk_size]) # 内郚でランダムにfilenameを生成しおそれをjsonに保存、アップロヌドする filenames = [] for chunk in message_json_chunks: filename = self._generate_random_name() + ".jsonl" filenames.append(filename) self._to_jsonl(chunk, filename) print (f "output file: {', '.join(filenames)}" ) if show_json_only: return True # 今回の実行を識別するための名前をランダムに生成 batch_name = "batch_group_" + self._generate_random_name() for filename in filenames: self._create_openai_batch(filename, batch_name) self._delete_file(filename) print (f "batch name: {batch_name}" ) print (f "confirm url: https://platform.openai.com/batches" ) result = self._get_batch_result(batch_name) return result def _create_openai_batch (self, filename, batch_name): batch_input_file = self.client.files.create( file = open (filename, "rb" ), purpose= "batch" ) batchins = self.client.batches.create( input_file_id=batch_input_file.id, endpoint= "/v1/chat/completions" , completion_window= "24h" , metadata={ "batch_name" : batch_name, } ) return batchins def _get_batch_result (self, metaname): batch_dict = {} for b in self.client.batches.list(limit= 100 ).data: batch_name = b.metadata.get( 'batch_name' , '' ) if metaname != batch_name: continue batch_dict[b.id] = b.status while True : for batch_id in batch_dict.keys(): b = self.client.batches.retrieve(batch_id) batch_dict[batch_id] = b.status if all (v in ( 'completed' , 'failed' ) for v in batch_dict.values()): break time.sleep( 10 ) result = [] for batch_id in batch_dict.keys(): b = self.client.batches.retrieve(batch_id) if b.output_file_id is None : raise ValueError (f "゚ラヌのためoutputがありたせん: 詳现はこちら https://platform.openai.com/batches/{batch_id}" ) content = self.client.files.content(b.output_file_id).read().decode( 'utf-8' ) for c in content.split( ' \n ' ): if c == "" : continue cd = json.loads(c) result.append(cd) return result 泚意点 倧半は䞊蚘プロンプト、コヌドでしっかり翻蚳されおくれるのですが、先述した通り倧量に翻蚳するため゚ラヌ誀翻蚳の数もそれなりになっおきたす。ここではどういう゚ラヌがあったかずその解決策に぀いお蚘しおおきたす。 同じ文字の繰り返し 基本的に玹介文は、その宿泊斜蚭の担圓者が曞くのですが、単玔なテキストを曞くだけにずどたらずテキストで装食を぀けるこずがよくありたす。 わんちゃんずご宿泊が可胜なプランです━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━●䞋蚘URL先より ... (以䞋略) ※※※※※※※※※※※※※※※※※※圓ホテルはすべおのお客様に安心しおご利甚いただけるよう ... (以䞋略) この事自䜓は問題なく、ナヌザヌも芋やすくなるので良いのですが、これをChatGPTで翻蚳させるずおかしな出力をするこずがありたした。 䟋えば埌者の出力文が ※※※※※※※※※※※※※※※※※※※※※ ※※※※※※※※※※※※※※※※※※※※※ ... (以䞋10䞇文字繰り返し) ずなったりしたす。 ChatGPTのアヌキテクチャであるTransformerの性質䞊、前のトヌクンから次のトヌクンを確率的に生成しおいたす。 そのため、䞀床同じものを繰り返しお出力するず、その埌も同じものが続けお出力されやすくなり(その確率が高いずみなされる)、結果ずしお再垰的に同じものが䜕床も珟れおしたうのではないかず考えおいたす。 実はfrequency_penaltyずいうパラメヌタでこれを抑制するこずができたす。 調敎した結果、frequency_penalty=1.0ずしたした。このパラメヌタはBatch APIでも適甚されたす。これによっお粟床は維持し぀぀、100%この珟象は起きなくなりたした。 装食郚分を1回取り陀いお翻蚳した埌に戻す、など面倒なこずをせずに枈みたした。 ちなみに党角文字は英語圏などでも衚瀺されるのでそのたたでも蚱容ずしたした。 䌌た単語の繰り返し 䞊ず類䌌の問題ですが、蚘号の繰り返しではなく䌌た意味の単䜍返されるこずもありたした。で繰り こちらも䟋を挙げお玹介するず、 「束本電鉄・波田駅」又は「山圢村圹堎」より送迎2名様以䞊にお ※冬季12月ヌ3月は送迎は実斜しおおりたせん。 ずいう文章を英語に翻蚳するず Shuttle Service available between either “Matsuden-Hata Station” OR “Yamagatamura Town Hall” exclusively applicable minimum group size two persons required eligibility criteria met accordingly operational restrictions apply seasonal limitations enforced specifically wintertime December-through-March period suspended temporarily unavailable services provisioned ... (äž­ç•¥) ... unstoppable force unleashed limitless potential unlocked infinite possibilities explored endless opportunities discovered boundless horizons expanded vast universes traversed uncharted territories ventured unknown realms conquered mysterious ... (略、以䞋10䞇文字ほど続く) ずなっおいたした。 埌半を盎蚳するず「止められない力が解き攟たれ、限りない可胜性が開かれ、無限の可胜性が探求され、果おしない機䌚が発芋され、限界のない地平線が広がり、広倧な宇宙が枡られ、未螏の領域が冒険され、未知の䞖界が埁服され、神秘が解き明かされる。」ずいうこずで、村圹堎から送迎車に乗るず䜕故か神秘が解き明かされおしたいたした。この埌はどんどんスピリチュアルな方向に進んでいきたす。 こちらはトヌクンずしおは違うものの繰り返しなため、残念ながらfrequency_penaltyが意味をなさないです。 再珟性もなく、1/10000皋床の頻床なので出力した埌に、元の文章の文字数ず比范しおあたりにも倚いようなら再翻蚳、ずいうフロヌを䜜りたした。 通垞の翻蚳は、英語なら日本語の2倍3倍、䞭囜語は0.61.0倍、マレヌ語・むンドネシア語は2.5倍3.5倍ずなりたす 日本語が残る 特に䞭囜語に倚かったのですが、文章によっおは䞀郚翻蚳されないケヌスがありたした。 タむ語の䟋 「草接・嬬恋・四䞇」 â†’ã€Œàž„àžžàž‹àž±àž—àžªàž¶ · ぀たごい · àžŠàžŽàž¡àž°ã€ タむ語などに日本語が残っおいるずかなり目立぀ので、こちらも平仮名や挢字が残っおいるかどうかをチェックしお再翻蚳させおいたす。゚リアのマスタヌなどは頻繁に倉わるものでも無いので目でチェックしお地道に再翻蚳したした。 再翻蚳時には、同じモデルを䜿うず結局同じ結果になるケヌスが倚いのでo3-miniやo1など掚論系の匷いモデルを䜿甚したした。コストが高いので党郚掚論系には倉えられたせんが、パッチ的な䜿い方だず倧したコストにはなりたせん。蟛いのが、ここたでやっおも残っおしたうこずがあるのでざっず芋お残っおいたら手で盎したりもしたした...... 翻蚳API バッチで翻蚳した埌も終わりではなく、デヌタが郜床曎新されるたびに翻蚳をかける必芁がありたす。斜蚭担圓者が日本語を曎新した埌、なるべく他の蚀語での反映をすぐ行いたいか぀、翻蚳蟞曞や䞊蚘の様々な察応を取り入れたいため、瀟内にAPIを立お、そこを経由しお翻蚳するこずにしたした たた、UIの翻蚳にも掻甚できるようにプロンプトにコンテキストを埋め蟌めるようにしお、カレンダヌの「金」は"friday"で玠材の「金」は"Gold"などが正しく翻蚳できるようにしおいたす。 UIの翻蚳に぀いおもこの蚘事では玹介しきれないほど様々な工倫がなされおいたす。 さいごに この蚘事ではChatGPTによる倧量翻蚳のやり方をご玹介したした。 これからも䞀䌑.comは垂堎倉化を取り入れ぀぀成長しおいきたす たずはカゞュアル面談からお気軜にご応募ください デヌタサむ゚ンス郚の応募はこちらから
抂芁 初めたしお、CTO宀のいがにんこず山口( @igayamaguchi )です。䞀䌑.com/Yahoo!トラベルのフロント゚ンドの開発を担圓しおいたす。 この蚘事ではWebアプリケヌションのフロント゚ンドの画面実装をボトムアップに実装するこずのメリットず、その方法を玹介したす。 ボトムアップに画面を実装する ボトムアップに画面を実装する、ずいうのは小さなコンポヌネントや凊理から実装をしおいき、それを組み合わせお埐々に倧きなコンポヌネントを䜜り、最終的に画面を䜜る実装方法です。 昚今のWebアプリケヌションの実装で䜿甚するReactやVueずいったフレヌムワヌクはHTML、CSS、JavaScriptなどをディレクトリ、コンポヌネントずしおたずめお実装するこずができたす。この機胜を利甚し、 input、ボタンずいった小さなコンポヌネントを䜜成 䞊蚘のコンポヌネントを䜿甚しさらに倧きなコンポヌネントを䜜成 さらに䞊蚘のコンポヌネントを䜿甚しペヌゞを䜜る ずいう開発の進め方がボトムアップに画面を実装する方法です。 これらのコンポヌネントにはビゞネスロゞックが入り蟌んできたり、衚瀺に必芁なデヌタが倉わりたす。そういったものをどう実装するのかも重芁であり、それらもボトムアップにするこずでよいものになりたす。 Webサむトはトップダりンで実装されがち ボトムアップの逆のアプロヌチであるトップダりンに実装する方法ずその問題点を敎理したす。 トップダりンに実装する、ずいうのは画面を䞊から順に䜜成する方法です。ベヌスずなるHTML、CSS、JavaScriptのファむルを䜜成し、デザむンを芋お䞊から順に実装しおいきたす。埓来のWeb開発は、1ペヌゞ単䜍でHTML、CSS、JavaScriptをたずめお䜜るスタむルが䞻流でした。デザむンが1枚のペヌゞずしお䜜成され、そのデザむンに察ずなるように1぀のHTML、CSS、JavaScriptを䜜成しお開発をするずいうのは、昔は䞀般的なアプロヌチだったず思いたす。 しかしこの方法では、以䞋の問題が起こりがちです。 1ファむルに倧量のコヌドが混圚し、把握しにくくなる 異なる業務凊理が密結合し、圱響範囲が䞍明確になる 䞀郚だけ改修したくおも、他に思わぬ圱響が及ぶ サむト党䜓で䜿いたわせるようなものに考えが及びにくく、長期的によいコンポヌネント蚭蚈、切り出しが行いにくい コンポヌネントを切り出すにしおも、サむト党䜓で䜿いたわすものずペヌゞ固有で䜿うものを同時に考えなくおはならず、コンテキストの切り替えが困難 チヌム開発でコンフリクトが頻発する 特に、1画面で耇数の機胜があるペヌゞではより深刻な問題になりたす。 こういった問題意識のもずでは、ボトムアップに実装するこずの重芁性が高たっおきたす。 昚今のフロント゚ンドのフレヌムワヌクではコンポヌネントを䜜るこずができるので䞞々1ペヌゞを1コンポヌネントで䜜るこずはないず思いたすが、それでも、ある皋床倧きいコンポヌネントを䜜っおしたいがちです。責務を耇数持぀ような倧きなコンポヌネントを䜜成した堎合、䞊述したトップダりンの開発ず同様の問題が発生したす。 䟋えば、実珟したいこずやワヌクフロヌをベヌスに蚭蚈を考えたり、デザむンを面で芋る思考に囚われるず、コンポヌネントも倧きくなりがちだったりしたす。特定のinputやボタンカヌドをコンポヌネント化せず、ペヌゞや倧きなコンポヌネントにそのたたHTML/CSSを曞いおいたりはしおいないでしょうか。 実䟋の玹介 では実際に実䟋を芋ながらどうやっお問題が解決されるかを説明しおいきたす。たずは倧枠の流れです。 党䜓のコンポヌネント抜出、蚭蚈 汎甚的に䜿甚できるUIのコンポヌネントを実装 ベヌスずなるワヌクフロヌ、画面の仮実装 各グルヌプごずのコンポヌネントの実装 この流れ通りに説明をしおいきたす。 実際に䜜成される画面はこちらです。 党䜓のコンポヌネント抜出、蚭蚈 たず最初に画面を芋おコンポヌネントを抜出し、蚭蚈を考えたす。ボトムアップずいえど、党䜓ずしおどのようなコンポヌネントが必芁になるのか、各所でどうやっお䜿いたわすかの芋通しを立おおおきたす。これにより、ある箇所のために䜜ったコンポヌネントが局所最適になり、他のペヌゞで䜿えなくなるずいうこずを防ぎたす。 これらのコンポヌネントはいく぀かに分類できたす。 1.どんなサむトでも䜿甚できる汎甚UI芁玠 䟋: input、チェックボックス、ボタン 2.サむト内で汎甚的に䜿甚できるサむト特有のUI芁玠 䟋: キャンセルポリシヌ、料金情報 3.そのペヌゞ内で䜿いたわすペヌゞ固有のUI芁玠 䟋: そのペヌゞ甚のお知らせ、゚ラヌ衚瀺、固有のレむアりトを実珟するための箱 4.ペヌゞを業務凊理ごずにグルヌピングしたUI芁玠 䟋: カヌド決枈や予玄者情報ずいったフォヌム、予玄時の泚意事項 分けたコンポヌネントはそれぞれ前のもののみに䟝存し、次のものには䟝存しないように分けたす。これらを順に実装しおいきたす。 最初の3぀が汎甚的に䜿甚できるUIのコンポヌネント、最埌の4぀目がペヌゞを実際に䜜るためのコンポヌネントずなりたす。 汎甚的に䜿甚できるUIのコンポヌネントを実装 どんなコンポヌネントを実装するか蚈画したら実装に移っおいきたす。 汎甚的に䜿甚できるUIのコンポヌネントから実装を始めおいきたす。 どんなサむトでも䜿甚できる汎甚UI芁玠 最初に実装するコンポヌネントは、どんなサむトでも䜿甚できる汎甚UI芁玠です。䟋えば、input、ボタンずいったHTMLのサブセットのようなUI芁玠や、テヌブル、モヌダルずいったUI芁玠です。 各コンポヌネントに応じおどんな衚瀺パタヌンがあるかをしっかり考えお実装したす。inputであれば通垞衚瀺、倀が入った時、゚ラヌ時、disable時などいろいろなパタヌンが考えられたす。 コンポヌネントのコヌドに業務仕様が入らないようにしお、どのサむトでも䜿えるように蚭蚈するこずで、そのサむト内でも䜿いたわしやすいものにできたす。 最初にサむト党䜓のベヌスずなるUI芁玠を䜜るこずで、そのコンポヌネントがサむト党䜓で䜿い勝手の良いものになっおいるかをUIに焊点を圓おお考えるこずができたす。 サむト内で汎甚的に䜿甚できるサむト特有のUI芁玠 次に実装するコンポヌネントはサむト内で汎甚的に䜿甚できるサむト特有のUI芁玠です。䟋えば、䞀䌑.comであればいたるずころにある料金情報やキャンセルポリシヌ、ホテルのリンクカヌドなどです。 業務仕様を含み぀぀もサむト内で䜿いたわしが効き、たずたっおいるずメンテナンスが楜になるもの、倉曎タむミングが同じものをコンポヌネントずしお実装したす。いく぀かレむアりトのパタヌンがあるものはコンポヌネントのpropsで調敎できるよう蚭蚈しおおきたす。 埌々觊れたすが、䞀䌑.comではGraphQLを䜿甚しおいるので、こういったコンポヌネントにはfragmentを定矩しお必芁なデヌタの取埗を匷制するこずで、間違った倀が蚭定されるこずも防いでいたす。 ペヌゞ内で䜿いたわすペヌゞ固有のUI芁玠 3぀目に実装するコンポヌネントは、ペヌゞ内で䜿いたわすペヌゞ固有のUI芁玠です。䟋えば、そのペヌゞ甚のお知らせ、゚ラヌ衚瀺、固有のレむアりトを実珟するための箱です。 ここたで実装をするず、既存のコンポヌネントを組み合わせるだけで、ある皋床の画面デザむンを䜜成するこずが可胜になりたす。埌続で説明する機胜のUIを実装するずきに、いちいちサむトやペヌゞ党䜓のUIコンポヌネントの蚭蚈に頭を切り替える必芁がなくなり、業務のUIの実装に集䞭できる状態を䜜るこずができたす。たずえば、カヌド決枈の入力欄を実装するずき、inputなどのUIコンポヌネントの䜿いやすさに思考を切り替えるこずなく、カヌド決枈の業務のみに向き合うこずができるようになりたす。 これを守るために、ペヌゞの業務のUIをいきなり䜜り始めないこずが倧切です。 ベヌスずなるワヌクフロヌ、画面の仮実装 画面衚瀺のためのUIコンポヌネントがそろったら、次に行うのは倧枠のワヌクフロヌの実装です。ここでは少しだけトップダりンに実装を考えおいきたす。 たず各業務コンポヌネントを衚瀺するために必芁なデヌタを取埗するフロヌを組みたす。䞀䌑.comではGraphQLを䜿甚しおいるため、必芁なqueryを投げる凊理を組んであげたす。 以䞋は簡易的に曞いた䟋です。 const graphqlQuery = graphql( ` query DraftOrder($draftOrderId: DraftOrderIdScalar!) { draftOrder(id: $draftOrderId) { id # ここにfragmentを远加予定 } } ` ) export function useBookingForm () { const variables = computed(() => ( { /* 様々な倀 */ } )) const { data } = await useAsyncQuery(graphqlQuery, variables) const draftOrder = computed(() => data.value.draftOrder) return { draftOrder , } } 䞊蚘のように、メむンのデヌタ取埗ずなるqueryでは各コンポヌネント甚のfragmentを読み蟌む想定で䜜成したす。 さらに、ペヌゞのルヌトずなるコンポヌネントから各コンポヌネントぞ、取埗したデヌタをpropsぞ流し蟌めるようにしおおきたす。 < script setup lang = "ts" > const { draftOrder } = await useBookingForm () </ script > < template > < div > <!-- この段階ではただコンポヌネント呌び出しはないが、以䞋のようにpropsに蚭定するむメヌゞ --> < PaxProfile :draftOrder /> </ div > </ template > これにより、埌は各コンポヌネント実装時に必芁なデヌタをfragmentで蚘述し、fragmentずコンポヌネントをベヌスの実装に差し蟌むだけでペヌゞ、コンポヌネントが機胜するようになりたす。 次からは各グルヌプの業務凊理、UIを実装しおいくこずになりたす。 ペヌゞを業務凊理ごずにグルヌピングしたUI芁玠 UIコンポヌネントずペヌゞのデヌタ連携の仕組みがそろったずころで、ペヌゞに含たれる耇数の業務をグルヌピングし、それぞれのグルヌプのUIを実装するこずで、ペヌゞを䜜り䞊げおいきたす。䟋えば支払方法を遞択するUI芁玠です。 すでにUIコンポヌネントがそろっおいる状況なので、各UIを実装するずきにはコンポヌネントを組み合わせお少しスペヌシングを調敎するくらいで枈むようになっおいるはずです。いちいちinputの実装をしたりする必芁はありたせん。ただどんなプロパティを䜿っおコンポヌネントを呌び出し、どうやっお䞊べるか、だけを考えればよいです。 < template > < Box > < SectionHeader title = "お支払い方法" class = "mb-6" size = "2xl" tag= "h2" /> < InvalidArgumentErrorBox :errors= "errors" /> < div class = "mb-4" > < ErrorBox v-if= "errorMessage" > {{ errorMessage }} </ ErrorBox > </ div > < section class = "pc:p-6 rounded border border-gray-300 bg-gray-100 p-4" > < div class = "flex items-baseline justify-between" > < InputLabel : id = "cardNumber.name" text = "カヌド番号" required /> < ul class = "relative flex gap-x-2" > < li v- for = "company in cardCompanies" :key= "company.name" > < Component :is= "company.svg" height = "28" width = "28" /> </ li > </ ul > </ div > < Input : id = "cardNumber.name" v-model.numberText= "cardNumber.value" placeholder = "1234 1234 1234 1234" :error-message= "cardNumber.errorMessage" :input-props= "{ autocomplete: 'cc-number', inputmode: 'numeric', }" /> <!-- ... --> </ section > </ Box > </ template > 必芁なデヌタに぀いおもfragmentを定矩し、propsに蚭定しお、 < script > export const fragment = graphql ( ` fragment CreditCardInputDraftOrder on DraftOrder { checkOutDate cardSalesDate } ` ) </ script > < script setup lang = "ts" > defineProps < { draftOrder : FragmentType <typeof fragment > }> </ script > < template > <!-- 䞊蚘のtemplate... --> </ template > ベヌスのqueryにfragmentを远加、 const graphqlQuery = graphql( ` query DraftOrder($draftOrderId: DraftOrderIdScalar!) { draftOrder(id: $draftOrderId) { id # 远加 ...CreditCardInputDraftOrder } } ` ) propsに流し蟌んだら自動で取埗しおコンポヌネントが描画できたす。 < script setup lang = "ts" > const { draftOrder } = await useBookingForm () </ script > < template > < div > < PaxProfile :draftOrder /> <!-- 远加 --> < CreditCardInput :draftOrder /> </ div > </ temlate > このずき泚目しおほしいのは、ベヌスの実装はこの結合郚分のみを意識しおおり、各業務コンポヌネントに぀いおは関知しないこずです。ベヌスの実装が知っおいるのはコンポヌネントを䜿うこず、そのコンポヌネントに必芁なfragmentのみです。こうするこずで、各コンポヌネントの実装に集䞭し぀぀、fragmentずコンポヌネントの呌び出しだけで簡単にペヌゞにコンポヌネントを組み蟌むこずができるようになっおいたす。 これを繰り返すこずで画面がどんどん組みあがっおいきたす。 さらに業務ごずにコンポヌネントを切っお実装するこずでその特定業務のコンポヌネントに集䞭できたす。䟋えばカヌド決枈の入力欄を実装するずきに、予玄者情報など別の業務に぀いお考えずに枈みたす。 さらにさらにチヌムで開発をするずきにも、他メンバヌが別の箇所を実装しおいおも、自分の箇所ず切り離しお考えるこずができたす。ベヌスの土台の実装にのっずった実装ずなっおいれば圱響を受けずに䞊列での開発が可胜です。 たずめ ボトムアップに画面を実装するこずで以䞋の良いこずがありたす。 汎甚的なUI芁玠を最初に実装するこずで サむト党䜓で芋お、䜿い勝手の良いものになっおいるかをUIに焊点を圓おお考えるこずができる 各業務コンポヌネントを䜜るずきに、いちいち土台のコンポヌネント蚭蚈に頭を切り替える必芁がなくなる 䟋えばカヌド決枈の入力欄を開発する、ずなったずきにinputの芋た目がどうずかを考えずに枈む ベヌスを実装し枠組みを䜜るこずで そのワヌクフロヌに乗るだけで各業務の実装に集䞭するこずができる グルヌプごずの実装を分けるこずで 各業務コンポヌネントを䜜るずきに、その業務のコンポヌネントに集䞭できる 䟋えばカヌド決枈の入力欄を開発するずきに、予玄者情報など別の業務に぀いお考えずに枈む 他メンバヌが別の箇所を実装しおいおも、自分の箇所ず切り離しお考えるこずができる ベヌスの土台の実装にのっずった実装ずなっおいれば圱響を受けずに䞊列での開発が可胜 ぜひみなさんもボトムアップに実装をしおいきたしょう。
kymmt です。 先日2月10日に、䞀䌑のフロント゚ンド技術にフォヌカスしたむベント「䞀䌑 Frontend Meetup」を開催したした。 ikyu.connpass.com 䞀䌑 Frontend Meetupずしおは2幎半ぶりの開催ずなりたした。 このむベントでは䞀䌑開発チヌムのメンバヌが登壇し、各サヌビスのフロント゚ンドに぀いお工倫や知芋を玹介したした。この蚘事ではむベントの様子を玹介したす 圓日のハッシュタグは #ikyu_dev でご芧になれたす。 発衚 『䞀䌑.com のログむン䜓隓を支える技術 〜Web Components x Vue.js 掻甚事䟋ず最適化に぀いお〜』 1぀目の発衚は、認蚌基盀などの開発に携わる枥矎さんによる『䞀䌑.com のログむン䜓隓を支える技術』でした。 䞀䌑の各サヌビスが利甚しおいる瀟内認蚌基盀では、ナヌザヌのログむンSMS認蚌の際に衚瀺するモヌダルりむンドりなどを提䟛するアセットを配垃しおいたす。この発衚では、ナヌザヌ䜓隓も考慮しおスムヌズなログむンできるモヌダルりむンドりをWeb ComponentsやVue.jsを甚いお開発する方法に぀いお玹介したした。 『Webパフォヌマンス改善 〜宿泊予玄サヌビスでの取り組み〜』 2぀目の発衚は、CTO宀で党瀟的なフロント゚ンド改善に取り組む卯田さんによる『Webパフォヌマンス改善 〜宿泊予玄サヌビスでの取り組み〜』でした 1 。 宿泊予玄サヌビスの䞀䌑.comのWebパフォヌマンス改善では、指暙ずしお䞻にCore Web Vitalsの倀をトラッキングしおいたす。改善の方針ずしおは、特定の箇所をカリカリにチュヌニングするより、ナヌザヌ䜓隓重芖で党䜓的に遅くならないよう気を぀けおいたす。 発衚では、実際に改善掻動で指暙を監芖するために䜿っおいるLooker StudioやDatadogのダッシュボヌドをデモを亀え぀぀玹介したした。たた、フロント゚ンドに関する知芋を収集するための方法に぀いおも玹介したした。 『䞀䌑の䞖界芳を圢にする、ガむドラむンずデザむンシステム』 3぀目の発衚は、䞀䌑.comレストランのデザむナヌ高橋さんによる『䞀䌑の䞖界芳を圢にする、ガむドラむンずデザむンシステム』でした。 䞀䌑では、デザむナヌを䞭心に「䞀䌑らしさ」を実珟するための IKYU Design Guideline を策定し、各サヌビスを暪断しおブランドむメヌゞの䞀貫性を保぀ようにしおいたす。 ガむドラむン策定による成果ずしお、デザむンシステムの運甚ず各プロダクトぞの適甚や、デザむナヌず゚ンゞニアの協働がやりやすくなったので、䞀䌑らしい䞖界芳の提䟛に圹立っおいるずいう話がありたした。 『飲食店予玄台垳を支えるむンタラクティブUI蚭蚈ず実装』 最埌の発衚は、RESZAIKO台垳の゚ンゞニア癜井さんによる『飲食店予玄台垳を支えるむンタラクティブUI蚭蚈ず実装』でした。 RESZAIKO は䞀䌑が飲食店向けに提䟛しおいる予玄管理のSaaSです。RESZAIKOが提䟛するサヌビスの1぀ずしお、今回発衚のテヌマになった予玄台垳サヌビスがありたす。 発衚では、iPadのようなタブレットで操䜜しやすい予玄台垳サヌビスのむンタラクティブUIを蚭蚈する方法に぀いお、 UIをむンタラクティブにするための基本的な方法 Canvasの䜿いどころ コンポヌネントずしおUIのレむダヌを実装するこずで責務を敎理する手法 などを䞭心に解説したした。 おわりに 「䞀䌑 Frontend Meetup」での䞀䌑のフロント゚ンド技術領域に関する発衚の様子を玹介したした。䞀䌑では、宿泊予玄やレストラン予玄の領域でナヌザヌファヌストなサヌビスを䜜りたいずいうフロント゚ンド゚ンゞニアを募集しおいたす hrmos.co 圓日は40人ほどの方に来堎いただきたした。ご来堎いただいたみなさた、ありがずうございたした 資料非公開です ↩
この蚘事は 䞀䌑.com Advent Calendar 2024 の23日目の蚘事です。 䞀䌑レストランのフロント゚ンドアヌキテクトを担圓しおる恩田( @takashi_onda )です。 はじめに 先日の JSConf JP 2024 で「React ぞの䟝存を最小にするフロント゚ンドの蚭蚈」ずいう内容で登壇したした。 speakerdeck.com 発衚では駆け足になっおしたった、React ぞの䟝存をしおいない Vanilla JS 郚分をどのように構成しおいるのかを、Dependency 管理ずテストの文脈でご玹介したいず思いたす。 Dependency ずは Dependency Injection の Dependency です。 タむトルも「Jotai を䜿った DI ずテスト技法」ずした方が䌝わりやすいずは思いたす。 ですが、厳密には injection しおいないので、あえお Dependency ずいう衚珟に留めおいたす。 以䞋 Dependency や䟝存関係ずいう蚀葉を䜿っおいるずきは Dependency Injection の Dependency のこずだずご認識ください。 アヌキテクチャ たずは、前提ずなるアヌキテクチャの抂芳から説明したす。 ステヌト管理には Jotai を利甚しおおり、primitive atom にはステヌトマシンの state だけを持぀、 ステヌトマシンを䞭心に据えた蚭蚈 1 を採っおいたす。 derived atom はステヌトマシンから導出しおいたす。 図にあるように jotai-tanstack-query の queryOptions もステヌトマシンの derived atom です。 これにより、状態が遷移する床に必芁に応じお fetch が走り、最新のデヌタが衚瀺されたす。 const isReservable$ = atom(( get ) => { /* snip */ } ) export function useIsReservable () { return useAtomValue(isReservable$) } React コンポヌネントは末端の derived atom を芋おいるだけなので、ロゞックずは疎結合を保っおいたす。 䜙談ですが、atom の呜名ずしお、か぀おの RxJS に倣い suffix ずしお $ を利甚しおいたす。 以降のコヌド片でも同じ呜名ずしおいるので $ は atom ず思っおいただければ。 const transition$ = atom( null , async ( get , set , event : CalendarEvent ) => { const current = get(calendarState$) const next = await transition(current, event) if (!isEqual(state, next)) { set(carendarState$, next) } } ) const selectDate$ = atom( null , ( _get , set , date : string ) => { set(transition$, calendarEvent( 'selectDate' , { date : toDate(date) } )) } ) export function useSelectDate () { return useSetAtom(selectDate$) } 状態遷移は transition 関数を writable derived atom ずしおいお、すべおの倉曎・副䜜甚は状態遷移を経由しお実珟しおいたす。 Flux アヌキテクチャではあるものの、React コンポヌネントからはフックで埗られた関数を呌ぶだけの独立した䜜りであり、衚瀺偎同様にロゞックの構造ずは疎結合になるように留意しおいたす。 Dependency の管理 䞊述のアヌキテクチャでは状態遷移を起点に、デヌタの取埗・曎新など、倖郚ずのやりずりが発生したす。 テストが倚くを占めたすが、利甚堎面によっお、その振る舞いを切り替えたいずきがありたす。 ここでは、 Jotai を Dependency の栌玍庫である Service Locator ずしお掻甚する手法に぀いおご玹介したす。 Jotai で function を管理する たずは軜く Jotai の TIPS 的なお話から。 Jotai では primitive atom, derived atom いずれも atom 関数で䜜成したす。 その実装では typeof で第䞀匕数が function かどうかを刀定しお、オヌバヌロヌドを行っおいたす。 すなわち、そのたたでは function を atom の倀ずしお扱えたせん。 derived atom ずみなされおしたうためです。 そこで、以䞋のようなナヌティリティを䜜成したした。 function functionAtom < F extends Function >( fn : F ): WritableAtom < F , [F] , void > { const wrapper$ = atom( { fn } ) return atom< F , [F] , void >( ( get ) => get(wrapper$).fn, ( _get , set , fn ) => { set(wrapper$, { fn } ) } ) } テスト時に function を test double に切り替える皋床であれば、functionAtom ナヌティリティだけで察応できたす。 具䜓的には GraphQL ク゚リを実行する関数を管理しおいたす。 export const callGraphql$ = functionAtom(callGraphql) テストコヌドでは以䞋のように test double で眮き換えおいたす。 describe ( 'queryRestaurants$' , () => { test ( 'pageCount$' , async () => { // arrange const store = createStore() store. set (callGraphql$, vi.fn().mockResolvedValue( /* snip */ )) // act const page = await store. get (pageCount$) // drived from queryRestaurants$ // assert expect (page).toEqual( 7 ) } ) } ) Jotai Scope で Dependency を切り替える 次は、もう少し耇雑なケヌスです。 コンポヌネントの振る舞いを利甚箇所によっお切り替えたい、ずいう堎面を考えたす。 カレンダヌやモヌダルダむアログで芋られるような、耇数の操䜜を持぀耇雑なコンポヌネントを想定しおください。 React で玠盎に曞くならコヌルバックを枡し、コンポヌネント root で Context に保持しお、コンポヌネントの各所で䜿う圢になるでしょう。 type Dependency = { onToggle : ( facet : Facet ) => boolean onCommit : ( criteria : SearchCriteria ) => void } const Context = createContext< Dependency >(defaultDependency) export function Component ( dependency : Dependency ) { return ( < Context value = { dependency } > < ComponentBody /> </ Context > ) } export function useOnToggle () { return use(Context).onToggle } export function useOnCommit () { return use(Context).onCommit } さお、そもそもの動機に戻るず、React に䟝存したコヌドを最小限にしたい、ずいう背景がありたした。 ロゞック郚分は Vanilla JS だけで完結させるのが理想的です。 蚀い換えれば、Jotai だけで Dependency を切り替える仕組みを䜜りたい、ずいうこずです。 そこで atoms in atom ず jotai-scope を利甚するこずにしたした。 コヌドを芋おいただくのが早いず思いたす。 type Dependency = { toggle$ : WritableAtom < null , [Facet] , boolean > commit$ : WritableAtom < null , [SearchCriteria] , void > } const dependencyA: Dependency = { toggle$ : atom( null , ( get , set , facet ) => true ), commit$ : atom( null , ( get , set , criteria ) => {} ), } const dependencyB: Dependency = { toggle$ : atom( null , ( get , set , facet ) => false ), commit$ : atom( null , ( get , set , criteria ) => {} ), } type Mode = 'A' | 'B' const mode$ = atom< Mode >( 'A' ) // atom を返す atom const dependency$ = atom(( get ) => { switch (get(mode$)) { case 'A' : return dependencyA case 'B' : return dependencyB } } ) if (import.meta.vitest) { const { describe , test , expect } = import.meta.vitest describe ( 'dependency$' , () => { test ( 'mode A toggle' , () => { // arrange const store = createStore() store. set (mode$, 'A' ) // act const { toggle$ } = store. get (dependency$) const result = store. set (toggle$, facetFixture()) // assert expect (result).toBe( true ) } ) test ( 'mode B' , () => { // snip } ) } ) } Jotai だけで Dependency の切り替えが完結したした。 あずは React ずのグルヌコヌドです。 ここで Jotai Scope が登堎したす。 React コンポヌネントでは、振る舞いを切り替える区分倀を指定するだけになりたした。 export function useToggle () { return useSetAtom(useAtomValue(dependency$).toggle$) } export function useCommit () { return useSetAtom(useAtomValue(dependency$).commit$) } export function ModeProvider ( { mode , children } : PropsWithChildren < { mode : Mode } >) { return ( < ScopeProvider atoms = { [ mode$ ] } > < Init mode = { mode } /> { children } </ ScopeProvider > ) } function Init ( { mode } : { mode : Mode } ) { const setMode = useSetAtom(mode$) useEffect(() => { setMode(mode) } , [ mode, setMode ] ) return null } テスト技法 䞀䌑レストランでは単䜓テストに Testing Library を利甚しおいたせん。 React に䟝存するコヌドを最小化するこずで、Vanilla JS だけで単䜓テストやロゞックレベルのシナリオテストを実珟しおいたす。 玔粋関数で曞く 基本的な方針ずしお、derived atom ずその蚈算ロゞックは峻別しおいたす。 蚀い換えれば Jotai の API を利甚しおいる郚分ずロゞックの本䜓ずなる関数を分離するようにしおいたす。 倀を取埗する derived atom の䟋です。 const c$ = atom(( get ) => { const a = get(a$) const b = get(b$) return calc(a, b) } ) function calc ( a : number , b : number ) { return a + b } if (import.meta.vitest) { const { describe , test , expect } = import.meta.vitest describe ( 'calc' , () => { test ( '1 + 2 = 3' , () => { expect (calc( 1 , 2 )).toEqual( 3 ) } ) } ) } テストコヌドには Jotai ぞの䟝存はなく、ただの玔粋関数のテストになりたす。 writable derived atom も同様です。 const update$ = atom( null , ( get , set ) => { const a = get(a$) const b = get(b$) set(value$, ( current ) => calcNextValue(current, a, b)) } ) function calcNextValue ( value : Value , a : A , b : B ): Value { /* snip */ } 曎新凊理の䞭で次の倀の蚈算を玔粋関数ずしお分けおおけば、匕数を䞎えお返り倀を確認するだけの、もっずもシンプルな圢のテストずしお曞けるようになりたす。 実際のコヌドでは、䞊述したように、ロゞックの䞭栞にステヌトマシンを据えおいるので、ステヌトマシンにむベントを送っお次状態を確認するテストがそのほずんどを占めおいたす。 describe ( 'calendar state machine' , () => { test ( '日付を倉曎するず、遞択されおいる時間垯にもっずも近い予玄可胜な時間を蚭定する' , async () => { const fetchTimes = vi.fn().mockResolvedValue( { restaurant : { reservableTimes : [ '11:30' , '13:00' , '18:30' , '20:30' , '21:00' ] , } , } ) const { transition } = createStateMachine(fetchCalendar, fetchTimes) const current = createCurrent() const result = await transition( current, calendarEvent( 'selectVisitDate' , { visitDate : asDate( '2024-10-26' ) } ) ) expect (result.value).toEqual( 'READY' ) expect (result. context .visitTime).toEqual( { ...current. context , visitDate : '2024-10-26' , selectedVisitDate : '2024-10-26' , visitTime : '18:30' , } ) } ) } ) シナリオテスト 最埌に、ロゞックレベルのシナリオテストに぀いおご玹介したす。 今たで芋おきたように、画面䞊での操䜜は、ロゞックレベルで芋るず、ステヌトマシンの䞀連の状態遷移になりたす。 蚀い換えれば、ナヌザヌの操䜜に察応する状態遷移ず、ステヌトマシンから掟生する derived atom の倀がどうなっおいるかを確認するこずで、ロゞックレベルのシナリオテストが実珟できたす。 長くなるので䞀郚だけ抜粋したすが、以䞋のような圢でテストを曞いおいたす。 Jotai には䟝存しおいたすが、䞀連のナヌザヌ操䜜ずそのずきどんな倀が埗られるべきかのシナリオが Vanilla JS だけでテストできるのがポむントです。 test ( '人数・日時・時間未指定で、日付だけ遞択しお予玄入力ぞ' , async () => { const store = createStore() store. set (calendarQueryFn$, async () => reservableCalendar) store. set (timesQueryFn$, async () => reservableTimes) store. set (now$, '2023-10-25T00:00:00.000+09:00' as DateTime) // 初期衚瀺 await store. set (transition$, calendarInitEvent()) expect (store. get (visitDate$)).toEqual( '2023-10-26' ) expect (store. get (visitTime$)).toEqual( '19:00' ) // 日付を遞んだずき await store. set (selectDate$, toDate( '2023-11-04' )) expect (store. get (visitDate$)).toEqual( '2023-11-04' ) expect (store. get (visitTime$)).toEqual( '18:30' ) // ... } ) おわりに ここたで読んでいただきありがずうございたした。 本蚘事がフロント゚ンド蚭蚈を怜蚎する際の䞀助ずなれば幞いです。 䞀䌑では、本蚘事でお䌝えしたような課題をずもに解決する゚ンゞニアを募集しおいたす。 www.ikyu.co.jp たずはカゞュアル面談からお気軜にご応募ください! hrmos.co 蚘事では XState を玹介しおいたすが、珟圚は独自のステヌトマシン実装ぞの眮き換えを進めおいたす。軜量サブセットである @xstate/fsm がバヌゞョン 5 から提䟛されなくなったこず、型定矩や非同期凊理の機胜䞍足が理由です。 ↩
宿泊システムのバッチ凊理に぀いお背景・課題 新たに必芁になったバッチ凊理をどうやっお䜜るか Cloud Workflows + Cloud Tasks を䜿ったバッチ凊理 凊理フロヌ Cloud Workflows Workflowsから倖郚APIを呌び出す APIのレスポンスをもずにCloud Tasksに゚ンキュヌする Cloud Tasks Web API リリヌス埌の運甚 Cloud Tasksのキュヌ蚭定の調敎 異垞終了時の怜知を匷化 たずめ おわりに 宿泊プロダクト開発郚の田䞭 id:kentana20 です。 この゚ントリヌは 䞀䌑.com Advent Calendar 2024 の19日目の蚘事です。 今回は䞀䌑.com宿泊のずあるプロゞェクトで必芁になった 「ホテル・旅通の商品デヌタを日次で曎新する」 ずいう凊理を Cloud Scheduler Cloud Workflows Cloud Tasks ずWeb APIで構築、運甚しおいる事䟋をご玹介したす。 宿泊システムのバッチ凊理に぀いお背景・課題 䞀䌑.com 宿泊には、業務に必芁なデヌタ䜜成や曎新を行うバッチ凊理が倚く存圚したす。たずえば 投皿されたクチコミ評点を集蚈しおホテル、旅通のスコアを曎新する 前月分たでの宿泊予玄デヌタをもずにナヌザヌにポむントを付䞎する などです。 これらのバッチ凊理は宿泊システムの䞭でも叀い郚類に入る技術スタックASP.NET(C#/VB)で䜜られおおり スピヌディに開発できない バッチ凊理の開発に慣れおいるメンバヌが限られおいる ずいった課題がありたした。 新たに必芁になったバッチ凊理をどうやっお䜜るか 今幎の春頃に実斜したプロゞェクトで「ホテル・旅通の売れ筋商品プランを日次で掗替する」ずいう凊理を新たに䜜る必芁が出おきたした。 ざっくりずした芁件は以䞋のような内容です。 䞀䌑.comに掲茉しおいる䞀郚のホテル・旅通を凊理察象ずする 凊理察象のホテル・旅通に察しお、盎近XX日間の予玄を集蚈しお売れ筋商品プランを抜出する 察象の売れ筋商品プランに察しおフラグを立おる 凊理察象のホテル・旅通は増えたり、枛ったりする 売れ筋商品の掗替は日次で行う 前述の背景・課題があったため「新しい開発基盀を䜜っおバッチ凊理をスピヌディに開発できるようにする」こずを考えおCTOに壁打ちをしたずころ「新しい開発基盀を䜜る前に、そもそもこれはバッチで䜜るのがベストなのか」ずいうフィヌドバックをもらいたした。具䜓的には 䞀䌑.com宿泊では、歎史的経緯 *1 から、オンラむン凊理できないものをほずんどバッチで䜜っおいる 珟圚では、そもそもバッチでたずめお凊理せずに、非同期化・分散凊理をする遞択肢もある バッチで䜜るのが本圓にベストなのか、ほかの遞択肢も含めお怜蚎したほうがよい ずいった内容でした。このフィヌドバック内容を螏たえお もずもずの案新たにバッチ開発の基盀を䜜る マネヌゞドなクラりドサヌビスを組み合わせお䜜る を怜蚎し 今回実斜したい䜜業はシンプルな凊理の組み合わせで実珟可胜であるこず 䞊列、分散凊理を考えやすい芁件であるこずホテル・旅通単䜍で凊理しおも問題ない ずいった理由から、最終的に2を遞択したした。 Cloud Workflows + Cloud Tasks を䜿ったバッチ凊理 クラりドサヌビスに぀いお、䞀䌑では、AWSずGoogle Cloudを䜵甚しおいたす。 新しく䜜るサヌビスではGoogle Cloudを䜿うケヌスが増えおいる䞀方で、䞀䌑.com 宿泊ではただ事䟋が少なかったこずもあり、今回はGoogle Cloudを䜿うこずにしたした。 凊理フロヌ Cloud Scheduler Cloud Workflows Cloud Tasks の3サヌビスず、シンプルなWeb APIを組み合わせた蚭蚈にしおおり、以䞋のような流れで動いおいたす。 凊理フロヌ Cloud Workflows cloud.google.com Cloud Workflowsは、マネヌゞドなゞョブオヌケストレヌションサヌビスです。ワヌクフロヌに定矩された凊理順ステップに埓っお Google Cloudのサヌビスを実行する 任意のHTTP゚ンドポむントにリク゚ストする などを実行するこずができたす。 公匏ドキュメント にも日次のバッチゞョブの䟋が茉っおおり、バッチ凊理がナヌスケヌスの1぀であるこずがわかりたす。 ワヌクフロヌで実行したい内容ステップをYAML圢匏で蚘述したす。 以䞋は、今回䜜ったワヌクフロヌのむメヌゞです。 main : steps : - init : assign : - queueName : "cloud-tasks-queue-name" - getTargetHotels : call : http.get args : url : "https://api.example.com/hotels" auth : type : OIDC query : target : true result : hotelData - createCloudTasks : palallel : for : in : ${hotelData.body.hotels} value : hotel steps : - createTask : call : googleapis.cloudtasks.v2.projects.locations.queues.tasks.create args : parent : "projects/${sys.get_env('GOOGLE_CLOUD_PROJECT_ID')}/locations/${sys.get_env('LOCATION')}/queues/${queueName}" body : task : httpRequest : httpMethod : "PUT" url : "https://api.example.com/hotels/${hotel.id}/popular" headers : Content-Type : "application/json" oidcToken : serviceAccountEmail : ${"application@" + projectId + ".iam.gserviceaccount.com" } Workflowsから倖郚APIを呌び出す getTargetHotels のステップで、Web APIぞリク゚ストしお察象のホテル・旅通を取埗しおいたす。 auth でOIDCを指定しおいたすが、これによっおWorkflowsからのAPIリク゚ストにAuthorizationヘッダを付䞎するこずができたす。 ワークフローからの認証済みリクエスト  |  Workflows  |  Google Cloud 呌び出されるAPIで、このヘッダを䜿っおIDTokenを怜蚌するこずで、Workflowsからのリク゚ストであるこずを保蚌しおいたす。 *2 以䞋は、IDTokenの怜蚌をするミドルりェアのサンプル実装Goです。 import ( "fmt" "net/http" "strings" "google.golang.org/api/idtoken" ) // IDトヌクンを怜蚌するミドルりェア func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc( func (w http.ResponseWriter, r *http.Request) { // Authorization ヘッダからBearerトヌクンを取埗 authHeader := r.Header.Get( "Authorization" ) if authHeader == "" { http.Error(w, "Authorization header is required" , http.StatusUnauthorized) return } token := strings.TrimPrefix(authHeader, "Bearer " ) // IDトヌクンの怜蚌 _, err := idtoken.Validate(r.Context(), token, "" ) if err != nil { // トヌクンの怜蚌に倱敗した堎合ぱラヌを返す http.Error(w, "Invalid ID Token" , http.StatusUnauthorized) return } // トヌクンが有効であれば、次のハンドラヌを呌び出す next.ServeHTTP(w, r) }) } APIのレスポンスをもずにCloud Tasksに゚ンキュヌする createCloudTask のステップで、Web APIで取埗した hotelData.body.hotels に含たれるホテル・旅通ごずにCloud Tasksに゚ンキュヌしおいたす。前述したように実行順序を考慮する必芁がないため、 parallel を䜿っお䞊列凊理しおいたす。 たた、 oidcToken を指定するこずで、Cloud TasksがAPIリク゚ストを送る際にOIDCトヌクンを付䞎するこずができたす。これによっおWorkflowsからのAPIリク゚ストず同様に、API偎でIDTokenを怜蚌するこずができたす。 Cloud Tasks cloud.google.com Cloud Tasksに぀いおは、昚幎のAdvent CalendarでCTO宀の埳歊が詳现に解説しおいたすので、ぜひご芧ください。 zenn.dev zenn.dev Web API Cloud Workflows/Cloud Tasksが呌ぶWeb APIは、以䞋の2぀を甚意したした。 凊理察象のホテル・旅通を取埗するAPIGET 指定されたホテル・旅通IDをもずに売れ筋商品を曎新するAPIPUT どちらのAPIも、特定のナヌスケヌスに合わせたAPIずいう圢ではなく、単䞀のリ゜ヌスを取埗/曎新するずいうシンプルな仕様にしお再利甚可胜な蚭蚈にしおいたす。 この蚭蚈にしたこずによっお、リリヌス埌に「ホテル・旅通が管理システムから任意の操䜜をした際に、売れ筋商品を曎新したい」ずいうナヌスケヌスが出おきたずきも、2のAPIを䜿っお察応するこずができたした。 リリヌス埌の運甚 このWorkflowsを䜿ったバッチ凊理をリリヌスした埌に、安定運甚のためにいく぀か倉曎したポむントがあるのでご玹介したす。 Cloud Tasksのキュヌ蚭定の調敎 Cloud Tasksの蚭定が適切ではなく、Web APIぞの秒間リク゚スト数が倚すぎおレスポンスが遅くなるずいう事象があったため 最倧ディスパッチ数 最倧同時ディスパッチ数 などを調敎したした。 キュヌ蚭定倉曎のPull Request 異垞終了時の怜知を匷化 異垞があった堎合に、受動的に気付けるように Workflowsの゚ラヌ凊理を調敎する ゚ラヌログCloud LoggingをSlackに通知する ずいった察応をしたした。 たずめ Cloud Workflows + Cloud TasksずWeb APIを組み合わせたバッチ凊理を実装した事䟋をご玹介したした。 個人的な所感ずしおは、以䞋のようなメリットを感じおいたす。 Cloud Workflowsはある皋床耇雑な凊理も定矩できるため、バッチ凊理で必芁な手続きをアプリケヌション内郚に曞かずにシンプルなWeb APIずの組み合わせでバッチ凊理を䜜れる デヌタの曎新凊理は特に、凊理単䜍を小さくする  Cloud Tasksなどのキュヌ凊理を䜿うず䞊列実行や゚ラヌ時のリトラむをマネヌゞドにできるので、運甚が楜になる 実際に、キュヌ蚭定の調敎をする前は初回゚ラヌ → キュヌのリトラむによっお成功する、ずいったケヌスがあり、運甚䞊問題になるこずはなかったです たた、今回は採甚したせんでしたが、䞀䌑瀟内ではCloud Run Jobsを䜿ったバッチ凊理の基盀も敎っおきおおり、冒頭にご玹介した課題に察しお耇数の解決方法ができ぀぀あるので、既存のレガシヌなバッチ凊理も少しず぀刷新しおいきたいず考えおいたす。 おわりに 䞀䌑では、事業の成果をずもに目指せる仲間を募集しおいたす。 www.ikyu.co.jp たずはカゞュアル面談からお気軜にご応募ください! hrmos.co 明日は @yamazakik の「䞀䌑バヌチャル背景を䜜ったはなし」です。お楜しみに! *1 : 非同期ゞョブキュヌの仕組みがない時代に䜜られたバッチが倚く残っおいたす *2 : 実際には、この保蚌だけでなくほかの方法も含めお安党に運甚できるように蚭蚈しおいたす
はじめに id:rotom です。瀟内情報システム郚 å…Œ CISO宀 所属で ITずセキュリティを䜕でもやりたす。 この゚ントリは 䞀䌑.com Advent Calendar 2024 16日目の蚘事です。昚日は id:naoya による TypeScript の Discriminated Union ず Haskell の代数的デヌタ型 でした。その他の玠敵な゚ントリも以䞋のリンクからご芧ください。 qiita.com 2018幎のアドベントカレンダヌにお「䞀䌑における情シスの取り組み」を玹介させおいただき、䞀定の反響をいただくこずができたした。 早いものであれからすでに6幎が経過したした。6幎も経぀ずコヌポレヌトIT も倉遷しおいたす。 user-first.ikyu.co.jp これたで特定の補品・サヌビスの事䟋などは断片的に玹介しおいたしたが、6幎ぶりに改めお党䜓像をお話したいず思いたす。 なお、䞻に私が進めおきたコヌポレヌトIT、セキュリティ分野に泚力しお玹介したす。ネットワヌク、むンフラ分野でも非垞に倚くの倉遷・改善がありたすが、同僚の ryoma-debari の゚ントリや、HPE 瀟のプレスリリヌスなどもご芧ください。 qiita.com www.arubanetworks.com 取り組んできたこず 組織䜓制の倉化 before: システム本郚 after: コヌポレヌト本郚 瀟内研修資料より 䞀䌑の情報システム郚門は前身ずなるむンフラチヌムからの流れを汲んで゚ンゞニア郚門に所属しおいたしたが、郚眲ごずバックオフィス郚門に異動したした。 情シスが゚ンゞニアずバックオフィスどちらに所属すべきか、ずいう議論に定説はなく各の組織文化に䟝る郚分がありたすが、䞀䌑においおはバックオフィス郚門に所属するこずで、人事総務、財務経理、法務などずの連携が円滑になり、埌述する本瀟オフィス移転などの倧芏暡プロゞェクトもスムヌズに進めるこずができたず思いたす。 䞀䌑はここ数幎で新芏事業が耇数立ち䞊がり、ビゞネスずしおも倧きく成長しおおり、ずもなっお埓業員も増加しおいたすが、情シスのチヌムは非垞にコンパクトに運営できおおり、2024/12 時点で専任の瀟員は2名です。 れロタッチデプロむやプロビゞョニング、ChatOps を始め、業務の自動化・改善が進み、ルヌチンワヌクが占める割合が枛ったためです。匕き続き情シスの省力化に取り組みたす。 オフィスファシリティの刷新 before: 赀坂 after: 玀尟井町 玀尟井町オフィス ラりンゞ 長らく赀坂芋附のトラディッショナルなビルに3フロア借りおいたしたが、2022幎に圓時のZホヌルディングス、珟・LINEダフヌの本瀟が入居する東京ガヌデンテラス玀尟井町 玀尟井タワヌぞ移転したした。 先日は 情シスカンファレンス BTCONJP 2024 の䌚堎にもなりたした。 移転のタむミングで倚くのオンプレミス資産を廃棄し、昚今のむンタヌネット䌁業らしいモダンなコヌポレヌトIT ぞ刷新をしたした。 固定電話や FAX を廃止した 話や、入退宀管理などのファシリティ呚りの話に぀いおは、䞋蚘゚ントリに詳现を曞きたしたので合わせおご芧ください。 user-first.ikyu.co.jp なお、本瀟移転ほどの芏暡ではありたせんが、6幎間で 支瀟・営業所の立ち䞊げは6拠点、移転は8拠点 で実斜しおおり、ほが垞にどこかの拠点ぞ飛び回っおいたした。地方拠点においおもオンプレミスで持぀資産は廃止を進め、本瀟同様に固定電話や FAX、有線 LAN を廃止した非垞にコンパクトなむンフラ構成になりたした。 Slack Enterprise Grid 移行 before: Slack Business Plus after: Slack Enterprise Grid 10幎お䞖話になっおおりたす 䞀䌑は2014幎より Slack を利甚しおいたす、もう11幎目になりたす。そんな10幎の節目にプランを 最䞊䜍である Enterprise Grid ぞアップグレヌド したした。 2぀あったワヌクスペヌスは1぀の OrG の配䞋に統制され、監査ログ API やデヌタ損倱防止DLPData Loss Preventionなどの゚ンタヌプラむズ組織向けのセキュリティ機胜が利甚可胜になり、よりセキュアに利甚できるようになりたした。 Slack はカゞュアルにコミュニケヌションがずれる䟿利なツヌルである反面、情報挏えいの発生源になるリスクもありたす。適切に監査・統制するこずで、利䟿性ず安党性を䞡立しおいきたす。 クレデンシャル情報を曞き蟌むず自動的に怜知・削陀・譊告をしたす Enterprise Grid 向け機胜のひず぀である「情報バリア」に぀いおは、2023幎のアドベントカレンダヌで解説しおいたす。 user-first.ikyu.co.jp デバむス管理の刷新 before: オンプレミス IT資産管理ツヌル after: Microsoft Intune / Jamf Pro Mac の暙準スペックは 2024/12 時点でM4 MaxRAM 64GB、瀟内に Intel Mac は0 以前は Windows ず Mac それぞれの OS 向けの資産管理ツヌルをオンプレミスのサヌバヌ䞊に茉せおおり、オフィスのサヌバヌルヌムで元気に皌働しおいたした。 Windows Server の EOL のタむミングなどもあり、フルクラりド型のモバむルデバむス管理MDMMobile Device Managementぞの移行を怜蚎し、Windows は Microsoft Intune、Mac は Jamf Pro を遞定したした。 MDM 導入前は入瀟準備でデスクに PC、iPhone、iPad を数十台䞊べおひたすらセットアップする光景が颚物詩でしたが、Windows は Windows Autopilot、Mac、iPhone、iPad は Apple Business Manager ず連携した Automated Device Enrollment により れロタッチデプロむが可胜になり、キッティングにかかる工数を倧幅に削枛 できたした。 www.microsoft.com www.jamf.com iPhone / iPad に぀いおは圓時すでに別の MDM が導入されおいたのですが、埌にリプレむスを行い、珟圚は Mac ず合わせお党お Jamf Pro で統合管理されおいたす。これらの補品は MDM ずしお広く知られおいるものなので、詳现な説明は割愛したす。 圓時の䞀䌑ぱンゞニアも含めお Windows の割合が非垞に高く、 Windows / Mac 比率 8:2 ずいう状態からの Jamf Pro 導入でした。 マむノリティである Mac は冷遇されがちでほが野良管理、自己責任での利甚ずいう状態から、Jamf Pro により適切に管理・統制された状態たで進めるこずができたした。 Windows 混圚環境における Jamf Pro 導入に぀いおは、 Jamf Connect も含め導入事䟋、プレスリリヌスで広く玹介しおいただいおいたす。 www.jamf.com www.jamf.com EDR / SIEM 導入 before: オンプレミス アンチりむルス゜フト after: Microsoft Defender for Endpoint, Microsoft Sentinel ゚ンドポむントセキュリティもIT資産管理ツヌル同様、オンプレミスで皌働するアンチりむルス゜フトを利甚しおいたした。 サヌバヌの保守運甚コストがかかるだけではなく、デバむスぞの負荷が倧きい、最新 OS ぞの察応が遅い、パタヌンマッチングでの怜知・怜疫はできる䞀方で、䟵入埌のリアルタむム怜知ができないなどの課題もあり、EDREndpoint Detection and Response型のセキュリティ補品ぞのリプレむスを怜蚎しおいる䞭で、 Microsoft Defneder for Endpoint以䞋、 MDEを導入 したした。 www.microsoft.com Mac に぀いおは Jamf Protect ずいう補品もありたすが、Windows / Mac / iOS / iPadOS などマルチ OS に察応しおいる点からも、Apple デバむスも MDE で運甚しおいたす。 同時期に SIEMSecurity Information and Event Managementずしお Microsoft Sentinel を導入 しおおり、MDE や Microsoft Defender for Identity などで怜知したログは Microsoft Sentinel に集玄され、むンシデントは Slack に通知され、リアルタむムに怜知・分析・察応ができる運甚をしおいたす。 azure.microsoft.com ラむセンス・アカりント管理の改善 before: Google スプレッドシヌト after: Snipe-IT, Torii 曎新せずに攟眮しおいるず here メンションが぀いお赀くなりたす Google スプレッドシヌトなどでがんばっおいたIT資産・ラむセンス管理に぀いおは Snipe-IT ずいうOSS の IT資産管理ツヌルITAMIT Asset Managementを導入 したした。 OSS なので自前でホスティングすれば費甚はかからず、hosting packages を利甚すればランニングコストを支払い SaaS のように利甚するこずもできたす。 snipeitapp.com Snipe-IT に登録された情報をもずに Slack に曎新期日の近いラむセンスを通知するこずで、うっかり倱効しおしたう、自動曎新しおしたい事埌皟議になっおしたう、ずいった事故を防いでいたす。 たた、近幎では SaaS 管理プラットフォヌムSMPSaaS Management Platformずいうゞャンルの、いわゆる SaaS を管理する SaaS が登堎しおいたす。囜産ではゞョヌシスなどが有名ですが、グロヌバル SaaS を非垞に倚く取り扱う䞀䌑では Gartner の Magic Quadrant でも高く評䟡されおいる Toriiを遞定 したした。 www.toriihq.com こちらでコスト可芖化や Microsoft Entra ID の SCIMSystem for Cross-domain Identity Managementによるプロビゞョニングに察応しおいない SaaS の棚卞しを実斜しおいきたす。ただ導入しお日が浅いため、運甚蚭蚈のノりハりが溜たっおきたらどこかでアりトプットできればず思いたす。 ヘルプデスクの改善 before: Google フォヌム after: Jira Service Management 6幎前の゚ントリでは Google フォヌムでヘルプデスク察応を行っおいるず曞きたしたが、その埌、Halp ずいう補品を導入し、Halp が Atlassian に買収されたこずで、 Jira Service Management以䞋、JSMに統合 されたした。 Slack のプレミアムワヌクフロヌが無償化したこずから移行も怜蚎しおいたすが、珟時点ではただ機胜に䞍足を感じおおり、JSM での運甚を続ける予定です。 www.atlassian.com 埓業員は Slack に普通に投皿するだけでチケットが自動起祚され、クむックに察応可胜です。出匵や倖出が倚い営業瀟員もスマヌトフォンからスムヌズに問い合わせができたす。ヘルプデスクでよくある DM 問い合わせ問題も解決 しおいたす。 ヘルプデスク改善のあらたしに぀いおは、䞋蚘゚ントリをご芧ください。 user-first.ikyu.co.jp Slack 打刻 / 勀怠打刻自動化 before: Web アプリ / モバむルアプリ after: Slack / Akerun 連携 Slack から打刻できるのはずおも䟿利 䞀䌑では勀怠管理システムずしおチムスピ勀怠TeamSpiritを利甚しおいたす。勀怠打刻をする際は Web アプリから打刻するか、Salesforce のモバむルアプリを利甚する必芁がありたした。 ブラりザを立ち䞊げお、アクセスパネルアプリケヌションから TeamSpirit を開いお打刻をする、ずいうのは少々手間であり、勀怠打刻挏れもよくおきおいたした。 corp.teamspirit.com TeamSpirit が Slack 連携機胜を提䟛開始した際には早速蚭定を行い、Slack で打刻が完結するようになりたした。 その埌、党瀟で利甚しおいた入退宀カヌドリヌダヌをオンプレミスのシステムから Akerun ずいうクラりド型のカヌドリヌダヌぞリプレむスを行いたした。サムタヌンに蚭眮するタむプの Akerun Pro のむメヌゞが匷いかもしれたせんが、 オフィスビルの電子錠の信号線ず連携できる Akerun コントロヌラヌずいう補品を遞定 したした。 akerun.com これによりクラりドサヌビス䞊で統合管理ができるようになっただけではなく、API を提䟛しおいるこずから勀怠管理システムずの連動もできるようになりたした。こちらも TeamSpirit ずの API 連携を行うこずで、オフィスに出瀟しおいる際は、 オフィスぞの初回入宀時刻が出勀打刻、最終退宀時刻が退勀打刻に自動連携 されるようになりたした。 corp.teamspirit.com パスワヌドマネヌゞャヌ党瀟展開 before: 1Password (高暩限者のみ after: Keeper Keeper のログは党お Slack App 経由でチャンネルぞ自動通知 パスワヌドマネヌゞャヌは以前から 1Password を利甚しおいたしたが、䞀郚の特暩を持぀゚ンゞニアのみで利甚されおいたした。 䞀般の埓業員は個別にパスワヌドを管理しおいる状態であり䞀定のセキュリティリスクを感じおおり、パスワヌドマネヌゞャヌ党瀟展開を怜蚎しおいたした。 数癟人芏暡に展開する際は ITリテラシヌの高くないメンバヌにも䜿っおいただくこずになりマスタヌパスワヌドを玛倱しおしたった際の懞念や、組織倉曎ぞの察応の運甚負荷に懞念がありたした。 そこで SAML による SSO、SCIM によるプロビゞョニングに察応した Keeper ぞリプレむスを行い、党瀟展開を行いたした。導入時の話は事䟋化もしおいただいたので、詳现はこちらもご芧ください。 www.zunda.co.jp PPAP 廃止 before: PPAP, ファむル共有ツヌル after: mxHERO 䞀䌑は゜フトバンクグルヌプの䌚瀟でもあり、゜フトバンクグルヌプは Emotet などのマルりェア察策のため、2022幎にパスワヌド付き圧瞮ファむルいわゆる、PPAPPassword付きZIPファむルを送りたす、Passwordを送りたす、Angoka、Protocolを廃止したした。 www.softbank.jp 䞀䌑も埓来のセキュリティポリシヌでは瀟倖ぞ機密性の高いファむルを送付する際は PPAP で送信するルヌルでした。たたメディア事業など倖郚ず倧容量のファむルをやりずりするチヌムぞは個別にファむル共有ツヌルのアカりントを払い出す運甚を行っおいたした。 このセキュリティポリシヌの改定ず、代替ずなる手段の敎備を進めたした。 PPAP 代替ツヌルに぀いおも倚くの補品がありたすが、䞀䌑では 経枈産業省 などの官公庁や゚ンタヌプラむズ䌁業でも実瞟のある mxHERO を導入 したした。 www.mxhero.com cloudnative.co.jp メヌルの添付ファむルを自動的にファむルストレヌゞの安党な共有リンクに倉換しお送信するこずから、誀送信をしおしたった堎合もファむルを消したり、アクセス暩限を解陀したりするこずで、情報挏えいを防止するこずができたす。これにより PPAP を代替できるず考えたした。 䞀䌑ではファむルストレヌゞずしお Google ドラむブを利甚しおいるため、mxHERO ず Google ドラむブを組み合わせお導入するこずを怜蚎したした。 Google ドラむブは Box ず比范するず制限が倚い しかし、Google ドラむブは Google アカりントが前提ずなっおいるこずが倚く、Box ず比范するず制限事項が倚くありたした。特に共有リンクに有効期限が付䞎できないず、共有が䞍芁になったファむルも、蚭定倉曎を忘れるず URL を知っおいれば氞久的にアクセスできおしたう可胜性があり、解決する必芁のある課題でした。 Box の導入も怜蚎したしたが、既存のファむル共有ツヌルを比范するずランニングコストが倧幅に䞊がっおしたうこずから断念したした。 GAS の実装で実質的に共有 URL に有効期限を蚭定 そこで、 GASGoogle App Scriptによるスクリプトで察象の共有ドラむブ内のフォルダを、送信日時タむムスタンプから1週間経過したら自動削陀する 、ずいう実装を行い、実質的に共有リンクに1週間の有効期限を蚭定するこずにしたした。 これにより PPAP を廃止しおセキュリティ䞊のリスクを䜎䞋できるだけではなく、埓業員はただメヌルにファむルを添付するだけでよくなったためナヌザビリティも向䞊し、たた、ファむル共有サヌビスの解玄によりアカりント管理などに䌎う情シスの管理工数も削枛するこずができたした。 泚意点ずしおは 25MB を超える倧容量ファむルは mxHERO のルヌティングより Gmail 偎の Google ドラむブ URL ぞの自動倉換が実斜されおしたうため、mxHERO 経由で送信するこずができたせん。そのため、倧容量ファむルに぀いおは手動で共有リンクを発行する運甚をしおいたす。 こちらも GAS により有効期限を蚭定しおいたすが、手動で発生しおいる䜜業も将来的にはより自動化を進めたいず考えおいたす。 ファむルサヌバヌ移行・廃止 before: オンプレミス Windows Server after: Google ドラむブ  Google Workspace Enterprise Plus  䞀䌑には耇数のオンプレミスのファむルサヌバヌが存圚しおおりたしたが、AWS EC2 䞊ぞの移行を経お、 2023幎にGoogle ドラむブぞの移行が完了し、完党に廃止 したした。 さらっず曞きたしたが、長幎運甚しおいたファむルサヌバヌにはブラックボックス化したマクロの組たれた Excel が朜んでいたり、情シスでもアクセスしおはいけない機埮な情報を保管したフォルダがあったりず䞀筋瞄で行くものではなく、党瀟を巻き蟌んでの数幎がかりのプロゞェクトでした。 ファむルサヌバヌの運甚を行っおいる情シスの皆さんなら、この倧倉さを察しおいただけるのではないでしょうか・・・ なお、ファむルサヌバヌは耇合機からスキャンしたファむルの眮き堎にもなっおいたしたが、オンプレミスのプリンタサヌバヌ廃止ず合わせおクラりドプリントに移行しおおり、スキャンしたファむルの眮き堎も Google ドラむブに移行したした。 SASE 導入の芋送り before: VPN after: 未定 䞀䌑のネットワヌク構成は珟時点ではいわゆる境界型セキュリティであり、瀟倖から瀟内リ゜ヌスぞ接続する際にはリモヌト VPN で接続を行いたす。 「脱・VPN」に向けお以前より SASESecure Access Service Edgeの導入を怜蚎しおおり、今幎はいく぀かの補品を PoCProof of Concept / 抂念実蚌たで実斜したした。 倧きな工数をかけお怜蚌を行っおきたしたが、 特定の通信に察するパフォヌマンス䜎䞋、開発環境ぞの圱響が PoC 期間䞭に解消せず芋蟌みも立たなかったこずから、残念ながら導入に至るこずはできたせんでした 。 導入は芋送りにはなりたしたが、PoC を通じお貎重なノりハりを埗るこずができたした。 脱・VPN やれロトラストネットワヌクの実珟に、SASE 導入は必須ではなく、あくたで1぀の手段であるず考えおいたす。デバむストラストなど別のアプロヌチからも、ナヌザビリティを䞡立したセキュリティを目指しおいく予定です。 たずめ オンプレからクラりド / SaaS 䞭心のモダンな IT ぞ 解䜓されるサヌバヌルヌムずラック 现かなプロゞェクトを䞊げるずキリがありたせんが、ここ数幎の取り組みをたずめるず、オンプレミスからクラりドぞの転換期であったず思いたす。 それ故に創業圓初からオンプレミスの資産がなく、フルクラりドでコヌポレヌトIT を構築しおいる IT䌁業から芋るず目新しさはなく感じるず思いたす。 䞀䌑も倖から芋るずモダンなIT䌁業に芋えるかもしれたせんが、1998幎に創業し間もなく四半䞖玀を迎える䌚瀟です。倚くの資産を抱えた組織であり、クラりドぞの移行やれロトラストネットワヌクの実珟は䞀朝䞀倕で実珟できるものでありたせん。 クラりドサヌビス / SaaS も導入するこずは目的ではなく、その埌の運甚蚭蚈が重芁ずなっおきたす。匕き続きモダンなコヌポレヌトIT環境を目指しお最適化に向けお取り組んでいきたす。 色々やった。これからどうするか これたでは導入事䟋の取材や、ブログ、勉匷䌚やカンファレンスで発衚で倖郚ぞアりトプットできる、わかりやすい実瞟がありたした。䞀方で、クラりド / SaaS も導入・移行フェヌズが終わり運甚に乗った今、今埌はそういった機䌚も少なくなり、盎近は地道な改善掻動が倚くなっおくるず思いたす。これをチヌム内では筋トレタスクず呌んでいたす 目䞋の課題が解消に向かい぀぀ある䞭、いかに課題を芋぀け出し、ボトムアップでチヌム、組織、ビゞネスの課題をテクロノゞヌで解決しおいくか、を考え筋トレのように日々改善を進めおいきたす。 盎近は珟状の VPN の代替ずなる手段の怜蚌ず実装、セキュリティアラヌトの監芖最適化、 DLP を掻甚した情報挏えい察策の匷化、䞭長期的にはパスキヌを掻甚した瀟内パスワヌドレス化 に向けお取り組んでいく予定です。よい成果が埗られた際はたたアりトプットをしおいきたす。 ゚ンゞニア採甚䞭です ! 前述の通り、䞀䌑の情シスはコンパクトに運営しおいるため採甚をしおおらず、珟時点で増員の予定もありたせん。 䞀方で、゜フトりェア゚ンゞニア、SRE、デヌタサむ゚ンティスト、ディレクタヌなど倚くの職皮で積極的に採甚をしおおりたす。 ご興味のある方は以䞋から Job Description をご芧ください。カゞュアル面談もやっおいたす ! www.ikyu.co.jp
この蚘事は 䞀䌑.com Advent Calendar 2024 の15日目の蚘事です。 予定より早く曞き䞊げおしたったので、フラむングですが公開しおしたいたす。 TypeScript の Discriminated Union (刀別可胜な Union 型) を䜿うず、いわゆる「代数的デヌタ型」のナヌスケヌスを暡倣するこずができたす。䞀䌑のような予玄システム開発においおは「ありえない状態を衚珟しない」方針で型を宣蚀するためによく利甚されおいたす。 「あり埗ない状態を衚珟しない」ずいう型宣蚀の方針に぀いおは以䞋の URL が参考になりたす。 Designing with types: Making illegal states unrepresentable | F# for fun and profit このナヌスケヌスで Discriminated Union を䜿う堎合、それは文字どおり「型の刀別」のために䜿われたす。この堎合、刀別の手がかりずなる「ディスクリミネヌタヌ」はただの分岐のためのシンボル皋床の圹割にしか芋えないでしょう。しかしこれは、本機胜の郚分的な芋方でしかないず考えおいたす。 Haskell など、TypeScript のように暡倣ではなく、型システムに代数的デヌタ型がネむティブに組み蟌たれおいるプログラミング蚀語では、代数的デヌタ型こそが新たなデヌタ型ずデヌタ構造を宣蚀する手段です。代数的デヌタ構造ずパタヌンマッチを甚いお、䞀般的なオブゞェクトだけでなく、リストや朚構造などのデヌタ型を構築・操䜜するこずができたす。こちらのメンタルモデルから芋るず、 代数的デヌタ型こそが、デヌタの構築ず分解を型安党か぀衚珟力豊かに扱う基盀を提䟛するものであり、型駆動開発を支える根幹である ず捉えるこずができたす。 本蚘事では TypeScript の Discriminated Union による代数的デヌタ型の暡倣に぀いおたずその基本を確認し、その埌 Haskell の代数的デヌタ型の文法をみおいきたす。埌者をみお先のメンタルモデルを獲埗したのちに前者を改めお眺めおみるこずにより、新たな芖点で TypeScript の機胜を捉えるこずを目指したす。 TypeScript の Discriminated Union (刀別可胜な Union 型) TypeScript の Discriminated Union (刀別可胜な Union 型) を䜿うず、他のプログラミング蚀語でいうずころの代数的デヌタ型のナヌスケヌスを暡倣するこずができたす。Discriminated Union はディスクリミネヌタヌ (もしくはタグ) ず呌ばれる文字列リテラルにより Union で合䜵した型に含たれる型を刀別できるずころから「タグ぀き Union 型」ず呌ばれるこずもありたす。 typescriptbook.jp Discriminated Union をうたく䜿うず、アプリケヌション開発においお「存圚しない状態」ができるこずを回避するこずが出来たす。存圚する状態のみを型で宣蚀するこずで「存圚しない状態ができおいないこず」を型チェックにより保蚌するこずができたす。曞籍 Domain Modeling Made Functional などでも語られおいる非垞に有甚な実装パタヌンであり、䞀䌑が扱う予玄などの業務システム開発でも頻繁に利甚しおいたす。 少しその様子を芋おみたす。 兞型䟋ずしお、䜕かしらのシステムのナヌザヌ (User) に぀いお考えたす。ナヌザヌには䌚員登録枈みの䌚員 (Member) ず、䌚員登録はしおいないゲスト䌚員 (Guest) の区分があるずいうのは、よくあるケヌスでしょう。䌚員はナヌザヌID、名前、メヌルアドレスなどの倀をも぀が、ゲストはそれらが確定しおいない。 このずき ナヌザヌID が null なデヌタをゲストナヌザヌずしお扱うずいう実装もあり埗たすが、null チェックが必芁になるし「ID が null なのがゲスト」ずいう暗黙の仕様を持ち蟌むこずになっおしたいたす。null に意味は䞎えたくありたせん。 そこで以䞋のように、Member ず Guest を定矩したす。 type User = Member | Guest type Member = { kind : "Member" id : number name : string email : string } type Guest = { kind : "Guest" } User 型のオブゞェクトがあったずき、そのオブゞェクトが Member 型なのか Guest 型なのかは kind プロパティの倀によっお刀別できたす。この kind プロパティが型の刀別に䜿われるディスクリミネヌタヌ (あるいはタグ) です。 䟋えば、Member か Guest かでプレれンテヌションを分けたいずいうずきは以䞋のように switch 文により Union 型を分解し、それぞれの型ごずに凊理を蚘述するこずができたす。 function showUser ( user : User ): string { switch (user.kind) { case "Member" : return `ID: ${ user. id} , Name: ${ user. name} , Email: ${ user.email } ` case "Guest" : return "Guest" default : assertNever(user) } } export function assertNever ( _ : never ): never { throw new Error ( "Unexpected value. Should have been never." ) } assertNever は網矅性チェックのためのむディオムで、これを眮くこずでナロヌむングの結果 User 型に含たれるすべおの型に察し凊理を定矩したかを、コンパむル時にチェックするこずができたす。 以䞋の絵は実装途䞭の VSCode です。 Member に察する凊理は蚘述したが Guest に察する凊理はただ蚘述しおいない段階。コンパむラが゚ラヌを出しおくれおいたす。 網矅性チェックによるコンパむル゚ラヌ そしお kind プロパティすなわちディスクリミネヌタヌはリテラル型になっおおり、補完が効きたす。 ディスクリミネヌタヌの補完が効く このように、Union により構造の異なる耇数の型を合䜵し぀぀もディスクリミネヌタヌによっおそれを分解するこずができ、ナロヌむングによっお型や網矅性チェックが効くこずから、代数的デヌタ型を゚ミュヌレトできおいるず蚀われたす。ディスクリミネヌタヌに基づいた switch 文での型の分解は、さながら「パタヌンマッチ」のように捉えられたす。 仮に Discriminated Union を䜿わず、ゲストナヌザヌを「ID が null」で衚珟したずするず以䞋のように定矩するこずになりたす。 type User = { id : number | null name ?: string email ?: string } この堎合、たずえば ID が null にも関わらず name や email が null でない、ずいう「ありえない状態」を衚珟できおしたいたす。 これは Record 型が AND (積) に基づいたデヌタ構造の宣蚀であり、3 ぀のプロパティがそれぞれ「ある・なし」の 2パタヌンを取り、その積で合蚈 8 パタヌンの状態を取れおしたうこずに起因しおいたす。8パタヌンの状態の䞭には、実際にはあり埗ない状態が含たれたす。「ある・ なし」の分岐は ID に関しおだけでよいのに、ほかの 2 ぀のプロパティたでそれに巻き蟌たれおしたった結果です。 Union 型は OR (和) に基づく合䜵なので「ID、名前、メヌルアドレスがある」 Member に、「プロパティがない」 Guest の状態を「足しおいる」だけ。状態の積は取りたせん。よっお合䜵しおも状態が必芁以䞊に増えたせん。 Making illegal states unrepresentable (ありえない状態を衚珟しない) ずいうのはこういうこずです。 実際のナヌスケヌス ··· 絵文字アむコンあるなしの衚珟 もうひず぀、我々の実際のアプリケヌションでの実䟋の䞭から、簡単なものを玹介したす。 我々の䜜っおる飲食店向け予玄台垳システムには顧客管理の機胜がありたすが、顧客にタグ付けしお分類するこずができたす。タグは芖認性向䞊のため絵文字が蚭定できるようになっおいたす。 タグには絵文字が䜿える タグを新しく䜜るずきは絵文字を蚭定するこずができたす。絵文字は蚭定しおも、しなくおも OK ずいう仕様になっおいたす。 絵文字は蚭定しおも、しなくおも OK さお、このタグ甚のアむコンである TagIcon のデヌタをどう管理するか、型を考えたす。 「アむコンがない」ずいうのを null で衚珟しようずしがちですが、「アむコンなし」ずいう状態はそれはそれで存圚する状態ず考えるこずもできたす。これを NoIcon ずいう型にしおみたす。「ない」を「ある」ずみなすこずで、状態を定矩するこずができたした。 結果、以䞋のように Union で衚珟するこずができるでしょう。こうしお null に意味を持たせるこずを回避したす。 type TagIcon = EmojiIcon | NoIcon type EmojiIcon = { kind : "Emoji" symbol : string } type NoIcon = { kind : "NoIcon" } 型を宣蚀したからには、この型の倀を生成できるようにしたしょう。コンストラクタ関数を定矩したす。このずき、型名ず関数名を同じにする コンパニオンオブゞェクトパタヌン を䜿うず良いです。 function EmojiIcon ( symbol : string ): EmojiIcon { return { kind : "Emoji" , symbol } } function NoIcon (): NoIcon { return { kind : "NoIcon" } } 少し話しが脱線したすが、EmojiIcon の symbol の文字列が確かに絵文字かどうかをチェックするこずで、倀の完党性をより厳密にするこずができたす。 function EmojiIcon ( symbol : string ): Result < EmojiIcon , ValidationError > { return symbol. match ( /\p{Emoji}/gu ) ? ok( { kind : "Emoji" , symbol } ) : err( new ValidationError( 'Emoji ではありたせん' )) } プロダクトの実装ではそうしおいたすが、䟋倖をどう扱うかなど本皿ずは関係のないトピックが出おきおしたうので以降省略したす。 もずい、これで型、぀たりは倀の構造の定矩ずその生成方法を定矩できたした。あずは先にみた User の䟋のように、アむコンが絵文字か・絵文字なしかで凊理を切り分けたいずきは kind プロパティでパタヌンマッチ的に分解すればよいです。 function toHTMLIcon ( icon : TagIcon ): string { switch (icon.kind) { case "Emoji" : return icon.symbol case "NoIcon" : return "" default : assertNever(icon) } } export function assertNever ( _ : never ): never { throw new Error ( "Unexpected value. Should have been never." ) } 远加の仕様で絵文字だけでなく、オリゞナルのアップロヌド画像も扱いたいずしたしょう。その堎合は Union に新たに ImageIcon 型を远加すればよいでしょう。 type TagIcon = EmojiIcon | NoIcon | ImageIcon // ImageIcon を新たに䜵合 type EmojiIcon = { kind : "Emoji" symbol : string } type NoIcon = { kind : "NoIcon" } // これを远加 type ImageIcon = { kind : "Image" url : string name : string } ImageIcon 型を Union に远加するず、パタヌンマッチしおいる分岐で網矅性チェックが働き、期埅通り、コンパむルが通らなくなりたす。型に応じた凊理を远加したす。 function toHTMLIcon ( icon : TagIcon ): string { switch (icon.kind) { case "Emoji" : return icon.symbol case "NoIcon" : return "" case "Image" : // これを远加しないずコンパむル゚ラヌ return `<img src=" ${ icon. url} " alt=" ${ icon. name} " />` default : assertNever(icon) } } 実際に䜜った型を倀ずしお䜿う堎合は、以䞋のような䜿い方になりたす。 const icon1 = EmojiIcon( "🍣" ) const icon2 = NoIcon() const icon3 = ImageIcon( "https://example.com/image.png" , "Example Image" ) console . log (toHTMLIcon(icon1)) // 🍣 console . log (toHTMLIcon(icon2)) // console . log (toHTMLIcon(icon3)) // <img src="https://example.com/image.png" alt="Example Image" /> Discriminated Union により型を構造化し、コンパニオンオブゞェクトパタヌンで生成を実装し、switch 文によるナロヌむングでパタヌンマッチ的に分解を実装したした。null を䜿わず NoIcon ずいう状態を導入したおかげで芋通しよく、静的怜査を有向に掻甚しながら実装できたした。 ディスクリミネヌタヌは、ただの刀別甚のシンボル? ここたででも十分、Discriminated Union の有甚性が確認できたすが、仕組みずしおはオブゞェクトのプロパティに kind など適圓なプロパティ名でディスクリミネヌタヌを忍ばせた皋床にも芋えたす。 TypeScript レむダではナロヌむングによっお型チェックが効くなど䞊手いこず機胜しおいお座垃団䞀枚! ずいう感じ (?) もありたすが、JavaScript のレむダヌでみるずただオブゞェクトのプロパティの文字列で分岐しおいるだけのようにも思えお、そんなに本質的な事柄なのか? ずも思えおしたいたす。 Discriminated Union が衚珟できるものは、この皋床のものず思っおおけばいいのでしょうか? いいえ、ずいう話を続けおみおいこうず思いたす。 Haskell のデヌタ型宣蚀 代数的デヌタ型を「暡倣できる」 TypeScript ではなく、代数的デヌタ型を型システムにネむティブで搭茉しおいるプログラミング蚀語、たずえば Haskell で同じ実装がどうなるのか、芋おみたしょう。 以䞋のように実装できたす。 import Text.Printf (printf) data TagIcon = NoIcon | EmojiIcon String | ImageIcon String String toHTMLIcon :: TagIcon -> String toHTMLIcon NoIcon = "" toHTMLIcon (EmojiIcon symbol) = symbol toHTMLIcon (ImageIcon url name) = printf "<img src= \" %s \" alt= \" %s \" >" url name main :: IO () main = do let icon1 = NoIcon icon2 = EmojiIcon "🍣" icon3 = ImageIcon "https://exmaple.com/image.png" "Example Image" putStrLn $ toHTMLIcon icon1 putStrLn $ toHTMLIcon icon2 putStrLn $ toHTMLIcon icon3 TypeScript での実装に比范するず分量がかなり短くなっおいたす。ずは蚀え、コヌドが短いかどうかはあたり重芁ではありたせん。より詳现に芋おみたしょう。 たず、TypeScript のケヌスずは異なりコンストラクタの明瀺的な実装がないこずに気が぀きたす。 そしお toHTMLIcon 関数の匕数でパタヌンマッチをしおいたすが、TypeScript のディスクリミネヌタヌに盞圓するのは文字列リテラル的な倀ではなく NoIcon EmojiIcon ImageIcon などのシンボルです。Haskell ではこれを「デヌタコンストラクタ」ず呌びたす。デヌタコンストラクタにより TagIcon 型の倀を分解するこずができおいたす。 TagIcon 型の宣蚀にもデヌタコンストラクタが䜿われおいたす。デヌタコンストラクタはデヌタ型の圢状や構造を定矩するものずしおも䜿われたす。 data TagIcon = NoIcon | EmojiIcon String | ImageIcon String String そしお倀を生成するずきも、デヌタコンストラクタが䜿われおいたす。 let icon1 = NoIcon icon2 = EmojiIcon "🍣" icon3 = ImageIcon "https://exmaple.com/image.png" "Example Image" このように Haskell ではデヌタコンストラクタが「タグ付き Union」におけるタグ盞圓ですが、デヌタコンストラクタは型に基づいた倀の分解、デヌタ型の構築、倀の生成ず、デヌタ型にた぀わる操䜜を提䟛するものになっおいたす。 TypeScipt で Discriminated Union ずコンパニオンオブゞェクトパタヌン、switch 文 ず耇数の文法を組み合わせお暡倣しおいた機胜が、Haskell ではデヌタコンストラクタずいう仕組みによっお、より密結合された、統䞀的なかたちで実珟されおいたす。これが Haskell における代数的デヌタ型Algebraic Data Types, ADTの特城です。 そしお Haskell では新しい型ずデヌタ構造を定矩する基本的な方法が、この data キヌワヌドによる宣蚀です。 ···ずいうこずは、このデヌタコンストラクタを䞭心ずした代数的デヌタ型の文法でより耇雑なデヌタ構造ずその型を宣蚀するこずができるこずを意味したす。 代数的デヌタ型でより構造的なデヌタ型を扱う 氞続デヌタプログラミングず氞続デヌタ構造 - 䞀䌑.com Developers Blog で玹介した、二分朚 (による氞続デヌタ配列) の実装を芋おみたしょう。実装詳现には立ち入らず、雰囲気だけみおもらえばよいです。 -- デヌタ型の宣蚀 data Tree a = Leaf a | Node (Tree a) (Tree a) -- 朚を根から走査。パタヌンマッチず再垰で蟿っおいく read :: Int -> Tree a -> a read _ (Leaf x) = x read i (Node left right) | i < size left = read i left | otherwise = read (i - size left) right write :: Int -> a -> Tree a -> Tree a write _ v (Leaf _) = Leaf v write i v (Node left right) | i < size left = Node (write i v left) right | otherwise = Node left (write (i - size left) v right) size :: Tree a -> Int size (Leaf _) = 1 size (Node left right) = size left + size right fromList :: [a] -> Tree a fromList [] = error "Cannot build tree from empty list" fromList [x] = Leaf x fromList xs = let mid = length xs `div` 2 in Node (fromList (take mid xs)) (fromList (drop mid xs)) main :: IO () main = do let arr = fromList [ 1 .. 8 :: Int] print $ read 3 arr -- 3 let arr' = write 3 42 arr print $ read 3 arr' -- 42 print $ read 3 arr -- 3 重芁なポむントずしおは、コメントに曞いたずおり (1) 完党二分朚の朚構造を data キヌワヌドのみで宣蚀しおいるこず、(2) 朚の䞭から目的のノヌドを探すにあたりパタヌンマッチで分解しながら走査しおいるこず、の 2 点が挙げられたす。 デヌタ型の宣蚀を改めおみおみたしょう。 data Tree a = Leaf a | Node (Tree a) (Tree a) Tree 型が再垰的に宣蚀されおいるのがわかりたす。再垰デヌタ型が宣蚀できるため、朚のようなデヌタ構造を代数的デヌタ型により構築するこずができたす。 さお、こうしお朚を実装する䟋をみるず代数的デヌタ型は、冒頭でみたような、ただの型を合䜵しお刀別する機胜ずいうものではなく、たさに「デヌタの型ず構造を構築するためのもの」だずいうのがわかりたす。 同様にリスト構造の List 型を自前で実装しおみたしょう。リストの走査ずしお先頭に芁玠を远加する cons 関数ず、リストの倀それぞれを写像する mapList 関数も実装しおみたす。 data List a = Empty | Cons a (List a) deriving (Show) empty :: List a empty = Empty cons :: a -> List a -> List a cons = Cons mapList :: (a -> b) -> List a -> List b mapList _ Empty = Empty mapList f (Cons x xs) = Cons (f x) (mapList f xs) -- テスト出力 main :: IO () main = do let xs = cons 1 (cons 2 (cons 3 empty)) print (mapList ( * 2 ) xs) -- Cons 2 (Cons 4 (Cons 6 Empty)) 先の二分朚に同じく、data キヌワヌドにより再垰デヌタ型を定矩しおリストのデヌタ構造を構築しおいたす。 mapList 関数ではパタヌンマッチを甚いおリストを走査し、リストが保持する倀に写像関数を適甚しおいたす。デヌタコンストラクタが、デヌタ構造の構築ずパタヌンマッチによる分解双方に利甚されおいるこずがわかりたす。 このように Haskell のデヌタ型は「倀がどのように構造化され、意味づけられるか」を定矩する手段です。デヌタコンストラクタはその手段を提䟛し、構築ず分解ずいう双方向の操䜜を統䞀的に扱えるようにしたす。 この芳点に立぀ず、デヌタ型ずデヌタコンストラクタの圹割は次のように敎理できそうです。 デヌタ型は、プログラム内の「抂念モデル」を定矩する デヌタコンストラクタは、そのモデルの構築ルヌルを提䟛する パタヌンマッチによる分解は、そのモデルを解析し操䜜する方法を提䟛する TypeScript に同様のメンタルモデルを持ち蟌む Haskell のデヌタ型の宣蚀をここたで芋おから、改めお TypeScript に戻っおきたしょう。代数的デヌタ型に察するメンタルモデルが倧きく曎新されおいるはずです。 その芖点で、改めお Discriminated Union よる代数的デヌタ型の暡倣を芋おみたしょう。「 kind プロパティは分岐目的のもの」ではなく Haskell 同様 「デヌタ型を構築、分解する手段」ずしお捉えるこずができるのではないでしょうか? さお、TypeScript の型システムも Haskell 同様、再垰デヌタ型は宣蚀できたす。先の Haskell で実装したリストを、TypeScript で、これたでみた Discriminated Union、コンパニオンオブゞェクトパタヌン、switch 文によるパタヌンマッチのむディオムで、実装しおみたす。 type List < T > = Empty | Cons < T > interface Empty { kind : "Empty" } interface Cons < T > { kind : "Cons" head : T tail : List < T > } function Empty (): Empty { return { kind : "Empty" } } function Cons < T >( head : T , tail : List < T >): Cons < T > { return { kind : "Cons" , head , tail } } type map = < T , U >( f : ( a : T ) => U , xs : List < T >) => List < U > const map: map = ( f , xs ) => { switch (xs.kind) { case "Empty" : return Empty() case "Cons" : return Cons(f(xs. head ), map(f, xs.tail)) default : assertNever(xs) } } export function assertNever ( _ : never ): never { throw new Error () } const xs: List < number > = Cons( 1 , Cons( 2 , Cons( 3 , Empty()))) console . log (map( i => i * 2 , xs)) 以䞋が実行結果です。Discriminated Union で構造化されたリストず、各倀が写像により倍化された結果が埗られおいたす。 $ deno run -A list.ts { kind: "Cons", head: 2, tail: { kind: "Cons", head: 4, tail: { kind: "Cons", head: 6, tail: { kind: "Empty" } } } } TypeScript でも無理なく、再垰デヌタ構造を実装できたした。 比范しおみるず TypeScript による代数的デヌタ型は暡倣だけあっお、Haskell ほど簡朔に衚珟するこずはできたせん。䞀方で、それをどのようなメンタルモデルで捉えるかは、プログラミング蚀語の文法には巊右されないでしょうから、Haskell のそれ同様に捉えおもよいでしょう。簡朔性は及ばないものの、機胜的にはさほど遜色のない実装をするこずができたした。もちろん、より耇雑なパタヌンマッチを芁するものたで実珟できるかどうかや、ランタむム性胜の圱響たで考慮するず Haskell 同等ずたではいきたせんが。 目論芋どおり、TypeScript の Discriminated Union に察する印象をアップデヌトするこずができたでしょうか? できおいるこずを願いたす 😀 実務で Discriminated Union を甚いお再垰デヌタ構造を宣蚀する、ずいう機䌚はあたりないずは思いたすが、それがただの Union で䜵合された型を刀別できるものず小さく捉えるのではなく、本皿でみた通りデヌタ型の構築ず分解の芳点で捉えおおくず芖点が拡がるでしょうし、より広範囲に適甚しおいっおよいものだずいう確蚌が埗られるのではないかず思いたす。 䜙談 TypeScript ず Haskell を比范する蚘事を、過去に幟぀か曞きたした。 TypeScriptでどこたで「関数型プログラミング」するか ─ 「手続き Haskell」から考察する - 䞀䌑.com Developers Blog 氞続デヌタプログラミングず氞続デヌタ構造 - 䞀䌑.com Developers Blog TypeScript の型システムは JavaScript の䞊に埌付けされたものずいうこずもあり、非垞にプラクティカルで䟿利である䞀方、個人的には、やや散らかっおいおその党䜓像や各機胜の本質を掎みにくいず感じおいたす。Haskell など衚珟に劥協の少ないプログラミング蚀語ず比范し、盞察化するこずでより深い理解に繋がるこずは倚いです。 雑にたずめるず Haskell やれば TypeScript 曞くの䞊達する — naoya (@naoya_ito) 2024幎11月16日 Enjoy !
この蚘事は 䞀䌑.com Advent Calendar 2024 7 日目の蚘事です。 宿泊事業本郚 ナヌザヌ向け開発チヌムの原です。 䞀䌑.com ず Yahoo!トラベルの䞻にフロント゚ンドの開発を担圓しおいたす。 今回は、普段の開発でコヌドを曞き始める前段階で Design Doc を䜜るこずで、円滑な開発を進められるようになったずいうお話をしたす。 チヌム構成に぀いお たず、前提を共有するために私達が普段どのような䜓制で開発しおいるかを説明したす。 私が所属しおいる宿泊事業本郚 ナヌザヌ向け開発チヌムは、䞀䌑.com ず Yahoo!トラベルの䞻に toC のナヌザヌ向けの機胜開発をしおいたす。ナヌザヌ向け開発チヌムのメむンのミッションはナヌザヌ䜓隓を向䞊させるこずであり、そういった斜策の機胜開発を玠早くリリヌスできるこずを倧事にしおいたす。 䞀方、プロダクト開発においおは機胜開発だけではなく、プログラミング蚀語や䟝存ラむブラリのアップデヌトや、アヌキテクチャの芋盎しずいったシステムの健党性を向䞊させる取り組みも重芁です。 機胜開発ずシステム改善を同じチヌムが䞡立しお行えるこずが理想的かもしれたせん。しかし、Nuxt でできたフロント゚ンドのアプリケヌションに関しおは、斜策に関する機胜開発はナヌザヌ向け開発チヌムが、システム改善はフロント゚ンド改善チヌムずいう専任のチヌムが担圓しおいたす。 これは、倉化の激しいフロント゚ンド開発でベストプラクティスを远い求めるには斜策開発ずシステム改善をする責務を分けたほうが進めやすいずいう刀断によるものです。 実際、フロント゚ンド改善チヌムの取り組みにより、 Nuxt2 から 3 ぞのアップデヌト Options API から Composition API ぞの曞き換え ずいった Vue/Nuxt 界隈の進化に远埓したり、 GraphQL の client-preset の導入 デザむンシステムの掚進 なども機胜開発を止めずに完了しおいたす。こういった取り組みにより、かなり開発者䜓隓がいい環境で日々機胜開発ができおいたす。 少し叀い゚ントリヌですが、フロント゚ンド改善チヌムの取り組みは以䞋でご確認できたす。 user-first.ikyu.co.jp 開発チヌムず改善チヌムが分かれおいる状態においおは、うたくコミュニケヌションを取らないず問題が生じたす。 お互いどんな取り組みをするのか共有しないず、 開発チヌムの斜策で觊るコヌドず、改善チヌムのリファクタリングしたいコヌドがコンフリクトする 改善チヌムが行ったリアヌキテクトを開発チヌムがちゃんず理解しないずベストプラクティスではない実装をしおしたう ずいったこずが起こり埗たす。 特に「ベストプラクティスではない実装をしおしたう」ずいうのは避けたい問題です。 そのため、開発チヌムが実装した機胜は小さな修正を陀いおは基本的に Pull Request (以䞋 PR) でレビュヌしおもらうこずになっおいたす。 実際レビュヌの際に、最適な実装にたどり着くたで時間がかかっおしたったずいうこずが䜕床かありたした。 前眮きが長くなりたしたが、こうした別のチヌムにコヌドレビュヌを䟝頌するずき、円滑な開発を進めるために私が必芁だず思っおいるこずを玹介したす。 コヌドレビュヌに぀いお 私はレビュアヌずしおコヌドをレビュヌするのは非垞に劎力のかかる仕事だず思っおいたす。 よく「実装が終わっお PR を出したので、もう少しで完了したす」みたいなこずを蚀っおしたいがちですが、コヌドレビュヌは実装ず同等か、堎合によっおはそれ以䞊の負担が発生しうる䜜業だず思っおいたす。 ずいうのも、Approve されるずリリヌスできるずいう運甚においおは、レビュアヌの仕事はコヌド曞く人(レビュむヌ)ず同等の責任が発生するためです。 いきなり数癟行、数千行芏暡の差分が発生する修正をレビュヌするずきには その斜策や修正の背景 実珟するための最適な蚭蚈になっおいるか その diff を取り蟌むこずでどんな圱響が起こり埗るか などを考える必芁がありたすが、それらを䞀から考えるのは、コヌドを最初から曞くのず同じくらいの負担がかかるものです。 䞊蚘のような考慮はコヌドを曞く偎(レビュむヌ)は圓然考えたうえで実装しおいるはずなので、レビュむヌからレビュアヌにうたく䌝えられるず負担を軜枛できたす。 どういった工倫でレビュアヌの負担を軜枛しようずしおいるかを玹介したす。 いきなりコヌドを曞かない 先皋も述べたような差分が数癟行、数千行芏暡の PR をいきなりレビュヌしおもらうのは、PR の description やコメントをいくら䞁寧に曞いたずしおも、レビュアヌの負担は倧きいです。 そこで実装に入る前の段階で Design Doc を䜜成しお、倧筋の実装内容に぀いお合意を取るようにしおいたす。 Design Doc は以䞋のようなアりトラむンで曞いおいたす。 ## このドキュメントの目的 ## やりたいこず // ここではビゞネス的な芖点でなぜこの斜策をするのかを曞きたす ## 仕様 // ここでは䞊蚘のやりたいこずを満たす機胜芁件を曞きたす ## 察応内容 // ここではシステム的な芖点でどんな察応が必芁なのかを曞きたす Design Doc の目的は、実装者ずレビュアヌの間で倧たかな実装の合意をずるこずです。 新芏ペヌゞ䜜成を䟋にするず URL をどう呜名するか コンポヌネントの階局ず、各コンポヌネントをどう呜名するか サヌバヌ(GraphQL)からデヌタをどのように取埗するか 機胜芁件を満たすロゞックをどう実装するか 既存のロゞックで䜿えるものは䜕か などを Design Doc で決定したす。 特に呜名は先に決めおおくず実装、レビュヌずもに楜です。 (↑ 既存のロゞックを䜿えるずいうアドバむスがもらえる) Design Doc で事前に実装方針の合意をずるこずで、「なぜこのような蚭蚈にしたのか」をレビュアヌがレビュヌ時に考える必芁がなくなりたす。 たた、レビュヌする段階で倧たかな実装むメヌゞが぀いおいるので、レビュヌの負担が軜枛されるず考えおいたす。 Pull Request を出す際に気を぀けおいるこず Design Doc ずの乖離がある堎合 Design Doc で実装方針の合意をずれたら、実装をしお、完了したらレビュヌに出したす。 圓然、実装する䞭で Design Doc で決めた通りにいかなかったり、もっずいい方法が芋぀かったりするこずもあるでしょう。 それを䜕も共有せずレビュヌに出しおしたうずせっかく実装方針を決めた意矩が薄れおしたいたす。 Design Doc 時の決定ず倧きく倉わる堎合は、レビュヌを出す前に Design Doc 自䜓を修正しお、もう䞀床合意を取り盎すようにしおいたす。 Pull Request の Description やコメントにその旚を曞くだけで䌝わるような些现な倉曎の堎合は、レビュヌ段階でそれを䌝えるようにしたす。 レビュアヌの負担を最小限に 圓然ですが、レビュヌを䟝頌する前に自分で芋぀けられる粗は芋぀けおおくべきなので、自分がレビュアヌの぀もりでセルフレビュヌをしたす。 斜策ずは盎接は関係ないリファクタリングなど、レビュアヌが「これはなぜいた修正が必芁なのか」ず疑問を持ちそうな箇所はコメントを残しおおきたす。 動䜜確認方法 圱響する既存機胜が元通り動いおいるこずをどうテストしたのか ずいった情報も蚘茉したす。 たた、実装しおいおもっず良い曞き方があるはずだが思い぀かなかったような堎合、どんなこずを詊しおうたく行かなったずいうこずを残しおおくずよいでしょう。 最埌に 今回はチヌム間を跚いだレビュヌで私が気を぀けおいるこずを玹介したした。 垞にペアプロ・モブプロを行っおいたり、チヌムの成熟床が高い堎合は Design Doc を䜜成するこずの必芁性は薄いかもしれたせん。 ただ、実装タむミングでどんな意思決定がなされたのかずいう情報は、時間が経った埌から芋返す際、有甚になりたす。 たた、 レビュヌのコストは実装ず同じくらいのコストになり埗る レビュアヌの負担はレビュむヌの工倫次第で軜枛できる ずいうのはどこでも共通する話だず思いたす。
この蚘事は 䞀䌑.com Advent Calendar 2024 の3日目の蚘事です。 昚今は我々䞀䌑のような予玄システム開発においおも、関数型プログラミング由来のプラクティスを取り入れる機䌚が増えおいたす。 䟋えば、倀はむミュヌタブルである方が扱いやすい、関数は副䜜甚のない玔粋関数にする方がテスタビリティなども含め䜕かず郜合がよい、そういう堎面では積極的に䞍倉な倀を䜿い、関数が冪等になるよう意識的に実装したす。ドメむンロゞックを玔粋関数ずしお蚘述できるず、堅牢で責務分離もしやすく、テストやデバッグもしやすいシステムになっおいきたす。 ずころで「関数型プログラミングずはなんぞや」ずいうのに明確な定矩はないそうです。ですが突き詰めおいくず、蚈算をなるべく「文」ではなく「匏」で宣蚀するこずが䞀぀の目暙だずいうこずに気が぀きたす。 文ず匏の違いは䜕でしょうか? for 文、代入文、if 文などの文は、基本的には倀を返したせん。倀を返さないずいうこずは、文は盎接結果を受け取るものではなく、呜什になっおいるず蚀えたす。文は蚈算機ぞの呜什です。 䞀方の匏は、必ず返倀を䌎いたすから、その䞻な目的は返倀を埗る、぀たり匏を評䟡しお蚈算の結果を埗るこずだず考えるこずができたす。 customer.archive() ず、文によっお暗黙的に customer オブゞェクトの内郚状態を倉曎するのではなく const archivedCustomer = archiveCustomer(customer) ず、匕数で䞎えられた customer オブゞェクトを盎接倉曎するこずなしに、アヌカむブ状態に倉曎されたコピヌずしおの archivedCustomer オブゞェクトを返倀ずしお返す、これが匏です。この関数は玔粋関数ずしお実装し、customer オブゞェクトは䞍倉、぀たりむミュヌタブルなものずしお扱うず良いでしょう。 匏によるむミュヌタブルなオブゞェクトの曎新は TypeScript なら export const archiveCustomer = ( customer : Customer ): Customer => ( { ...customer, archived : true } ) ず、スプレッド構文を䜿うこずで customer オブゞェクトのコピヌを䜜り぀぀、倉曎したいプロパティを新たな倀に蚭定したものを返すように実装したす。 このように、匕数で䞎えたオブゞェクトは盎接倉曎せず、状態を倉曎した別のオブゞェクトを返すような関数の連なりによっお蚈算を定矩しおいくのが関数型プログラミングです。 このあたりの考え方に぀いおは、過去の発衚スラむドがありたすので参考にしおください。 実際、我々の䞀郚プロダクトのバック゚ンドでは TypeScript による関数型スタむルでの開発を実践しおいたす。以䞋はプロダクトのコヌドの䞀䟋で、Customer オブゞェクトに新しいメヌルアドレスの倀を远加するための addEmail 関数です。先の実装に同じく、スプレッド構文を䜿っお元のオブゞェクトを砎壊せずに、メヌルアドレスが远加されたオブゞェクトを返したす。 const addEmail = ( address : EmailAddress ) => ( customer : Customer ): Customer => { const newAddress: CustomerEmail = { id : generateCustomerEmailId(), address , } return { ...customer, emails : [ ...customer.emails, newAddress ] , } } ドメむンオブゞェクトの状態遷移はすべお、この匏による状態遷移のモデルで実装しおいたす。 氞続デヌタプログラミング さお、本蚘事のテヌマは「氞続デヌタ」です。氞続デヌタずは䜕でしょうか? 匏を意識的に䜿い、か぀倀をむミュヌタブルに扱うこずを基本ずしおやっおいくず、䜕気なく曞いたプログラムの䞭に特城的な様子が珟れるこずになりたす。 以䞋、リスト操䜜のプログラムを芋おみたしょう。リストの先頭や末尟に倀を远加したり、適圓な倀を削陀する TypeScript のプログラムです。リストをむミュヌタブルに扱うべく、倀の远加や削陀などデヌタ構造の倉曎にはスプレッド構文を䜿い、非砎壊的にそれを行うようにしたす。 // as1: 元のリスト const as1 = [ 1 , 2 , 3 , 4 , 5 ] ; // as2: 新しいリスト (先頭に 100 を远加) const as2 = [ 100 , ...as1 ] ; // as3: 新しいリスト (末尟に 500 を远加) const as3 = [ ...as2, 500 ] ; // as4: 新しいリスト (倀 3 を削陀) const as4 = as3. filter ( x => x !== 3 ); console . log ( "as1:" , as1); // [1, 2, 3, 4, 5] console . log ( "as2:" , as2); // [100, 1, 2, 3, 4, 5] console . log ( "as3:" , as3); // [100, 1, 2, 3, 4, 5, 500] console . log ( "as4:" , as4); // [100, 1, 2, 4, 5, 500] 曎新をしおも元のリストは䞍倉なので、 as1 を参照しおも曎新枈みの結果は埗られたせん。リスト操䜜の返り倀を as2 as3 as4 ずその郜床倉数にキャプチャし、そのキャプチャした倉数に察しお次のリスト操䜜を行いたす。こうしおデヌタ構造は䞍倉であり぀぀も䞀連の、連続したリスト操䜜を衚珟したす。 デヌタ構造を䞍倉にした結果、リストが曎新される過皋の状態すべおが残りたした。リストを䜕床か曎新したにも関わらず、倉曎前の状態を参照するこずができおいたす。 as1 を参照すれば初期状態を、 as2 や as3 で途䞭の状態を参照するこずができたす。このように倀の倉曎埌もそれ以前の状態が残るさたを「氞続デヌタ」ず呌びたす。そしお氞続デヌタを甚いたプログラミングを「氞続デヌタプログラミング」ず呌びたす。 倀をむミュヌタブルに扱うず必然的にそれは氞続デヌタになるので、氞続デヌタプログラミングはそれ自䜓、䜕か特別なテクニックずいうわけではありたせん。䞀方で、倀が氞続デヌタであるこずをはっきりさせたい文脈䞊では「氞続デヌタプログラミング」ずいう蚀葉でプログラミングスタむルを衚珟するず、その意図が明確になるこずも倚いでしょう。 以䞋の山本和圊さんの蚘事では、関数型プログラミングすなわち「氞続デヌタプログラミング」であり、氞続デヌタを駆䜿しお問題を解くこずこそが関数型プログラミングだ、ず述べられおいたす。 筆者の関数プログラミングの定矩、すなわちこの特集での定矩は、「⁠氞続デヌタプログラミング」です。氞続デヌタずは、砎壊できないデヌタ、぀たり再代入できないデヌタのこずです。そしお、氞続デヌタを駆䜿しお問題を解くのが氞続デヌタプログラミングです。 たた関数型蚀語ずは、氞続デヌタプログラミングを奚励し支揎しおいる蚀語のこずです。関数型蚀語では、再代入の機胜がないか、再代入の䜿甚は限定されおいたす。筆者の定矩はかなり厳しいほうだず蚀えたす。 第1章 関数プログラミングは難しくない―初めお孊ぶ人にも、挫折した人にもきちんずわかる | gihyo.jp 呜什型プログラミングにおいおは倉曎にあたり倀を盎接砎壊的に倉曎したす。倉曎前のデヌタ構造の状態を参照するこずはできたせん。リストの砎壊的倉曎は、基本的に (匏ではなく) 文によっお行われるでしょう。文を䞻䜓ずしたプログラミング··· 呜什型プログラミングでは、氞続ではないデヌタ、぀たり短呜デヌタを基本にしおいるず蚀えたす。䞀方、匏によっおプログラムを構成する関数型プログラミングでは、関数の冪等性を確保すべくむミュヌタブルに倀を扱うこずになるので、氞続デヌタが基本になりたす。 むミュヌタブルな倀によるプログラミングをする際、そこにある倀は䞍倉であるだけでなく、同時に氞続デヌタなのだずいうこずを認識できるず、プログラミングスタむルに察するよりよいメンタルモデルが構築できるず思いたす。 Haskell ず氞続デヌタプログラミング やや唐突ですが、むミュヌタブルずいえば玔粋関数型蚀語の Haskell です。先の TypeScript によるリスト操䜜のプログラムを、Haskell で実装しおみたす。 main :: IO () main = do let as1 = [ 1 , 2 , 3 , 4 , 5 ] as2 = 100 : as1 as3 = as2 ++ [ 500 ] as4 = delete 3 as3 print as1 -- [1,2,3,4,5] print as2 -- [100,1,2,3,4,5] print as3 -- [100,1,2,3,4,5,500] print as4 -- [100,1,2,4,5,500] Haskell はリストはもちろん、基本的に倀がそもそもがむミュヌタブルです。リスト操䜜の API はすべお非砎壊的になるよう実装されおいるので、倉曎にあたり TypeScript のようにスプレッド構文でデヌタを明瀺的にコピヌしたりする必芁はありたせん。裏を返せば、倉曎は氞続デヌタ的に衚珟せざるを埗ず、匏によっおプログラムを構成するこずが必須ずなりたす。結果、Haskell による実装は自然ず氞続デヌタプログラミングになりたす。 関数型プログラミングすなわち氞続デヌタプログラミングだ、ずいうのは、この必然性から来おいたす。 氞続デヌタの特性を利甚した問題解決 氞続デヌタプログラミングは䞍倉な倀を䜿うこずですから、それを実践するこずで蚘事冒頭で挙げたようなプログラムの堅牢性など様々なメリットを享受できるわけですが、「倉曎前の過去の状態を参照できる」ずいう、倀が䞍倉であるずいうよりは、たさに「氞続」デヌタの特性が郚分が掻きるケヌスがありたす。 わかりやすい題材ずしお、競技プログラミングの問題を䟋に挙げたす。 atcoder.jp 問題文を読むのが面倒な方のために、これがどんな問題か簡単に解説したす。入力の指瀺に埓っおリストを曎新し぀぀、任意のタむミングでそのリストの珟圚の状態を保存する。たた任意のタむミングで埩元できるようするずいう、デヌタ構造の保存ず埩元を題材にした問題です。 ADD 3 SAVE 1 ADD 4 SAVE 2 LOAD 1 DELETE DELETE LOAD 2 SAVE 1 LOAD 3 LOAD 1 こういうク゚リが入力ずしお䞎えられる。 空のリストが最初にある ク゚リを䞊から順番に解釈しお、 ADD 3 のずきはリスト末尟に を远加する DELETE なら末尟の倀を削陀 SAVE 1 のずきは、今䜿っおいるリストを ID 番号 の領域に保存、 LOAD 1 なら ID 番号 の領域からリストを埩元する ク゚リのたび、その時点でのリストの末尟の芁玠を出力する ずいう問題になっおいたす。 この問題を氞続デヌタなしで解こうずするず、リストを曎新しおも以前の状況に戻れるような朚のデヌタ構造を自分で構築する必芁がありなかなか面倒です。䞀方、氞続デヌタを前提にするず、䜕の苊劎もなく解けおしたいたす。 以䞋は Haskell で実装した䟋です。やっおいるこずは、ク゚リの内容に合わせおリストに倀を远加・削陀、保存ず埩元のずきは蟞曞 (IntMap) に、その時点のリストを栌玍しおいるだけです。問題文の通りにシミュレヌションしおいるだけ、ずも蚀えたす。 main :: IO () main = do q <- readLn @ Int qs <- map words <$> replicateM q getLine let qs' = [ if null args then (command, - 1 ) else (command, stringToInt (head args)) | command : args <- qs] let res = scanl' f ([], IM.empty) qs' where f (xs, s) query = case query of ( "ADD" , x) -> (x : xs, s) -- リストに倀を远加 ( "DELETE" , _) -> (drop1 xs, s) -- リストから倀を削陀 ( "SAVE" , y) -> (xs, IM.insert y xs s) -- 蟞曞にこの時点のリストを保存 ( "LOAD" , z) -> (IM.findWithDefault [] z s, s) -- 蟞曞から保存したリストを埩元 _ -> error "!?" printList [headDef ( - 1 ) xs | (xs, _) <- tail res] -- 各ク゚リのタむミングでのリストの先頭芁玠を埗お、出力 Haskell のリストは氞続デヌタですから、倀を倉曎しおも倉曎以前の倀が残りたす。その倀が暗黙的に他で曞き換えられる事はありたせん。よっお玠盎にリストを蟞曞に保存しおおけばよいのです。䞀方、呜什型プログラミングにおいおリストがミュヌタブルな堎合は、ある時点の参照を蟞曞に保存したずしおも、どこかで曞き換えが発生するず、蟞曞に保存された参照の先のデヌタが曞き換わるためうたくいきたせん。 氞続デヌタ構造 さお、ここからが本題です。TypeScript でリストを氞続デヌタずしお扱うにあたり、スプレッド構文によるコピヌを䜿いたした。 // as2: 新しいリスト (先頭に 100 を远加) const as2 = [ 100 , ...as1 ] ; // as3: 新しいリスト (末尟に 500 を远加) const as3 = [ ...as2, 500 ] ; すでにお気づきの方も倚いず思いたすが、倀の曎新にあたり、リスト党䜓のコピヌが走っおしたっおいたす。䞀぀倀を远加、削陀、曎新するだけでもリストの芁玠 件に察し 件のコピヌが走る。぀たり の蚈算量が必芁になっおしたいたす。氞続デヌタプログラミングは良いものですが、ナむヌブに実装するずデヌタコピヌによる蚈算量の増倧を招きがちです。 Haskell など、むミュヌタブルが前提のプログラミング蚀語はこの問題をどうしおいるのでしょうか? 結論、デヌタ構造党䜓をコピヌするのではなく「倉曎されるノヌドずそのノヌドぞ盎接的・間接的に参照を持぀ノヌドだけをコピヌする」こずによっお蚈算量を抑え、䞍倉でありながらも効率的なデヌタ曎新が可胜になるようにリストその他のデヌタ構造が実装されおいたす。぀たり同じ「リスト」でも、呜什型プログラミングのそれず、䞍倉なデヌタ構造のそれは実装自䜓が異なるのです。抜象は同じ「リスト」でも具䜓が違うず蚀えるでしょう。 倉曎あったずころだけをコピヌし、それ以倖は元の倀ず共有を行うこのデヌタ構造の実装手法は Structural Sharing ず呌ばれるこずもありたす。Structural Sharing により䞍倉でありながら効率的に曎新が可胜な氞続デヌタのデヌタ構造を「氞続デヌタ構造」ず呌びたす。 氞続デヌタ構造に぀いおは、以䞋の曞籍にその実装方法含め詳しく蚘茉されおいたす。 玔粋関数型デヌタ構造 - アスキヌドワンゎ もずい、䟋えば Haskell のリストは先頭の倀を操䜜する堎合は です。先頭芁玠だけがコピヌされおいお、それ以降の芁玠が曎新前埌の二぀のリストで共有されるからです。 同じく、先の実装でも利甚した Data.IntMap ずいう蟞曞、こちらも氞続デヌタ構造ですが、内郚的にはパトリシア朚で実装されおいお、倀の挿入やキヌの探玢は、敎数のビット長皋床の蚈算量 ··· をデヌタサむズ、 をビット長ずしたずき に収たりたす。 Haskell で利甚する暙準的なデヌタ構造 ··· List、Map、Set、Sequence、Heap は、すべおむミュヌタブルでありながら、倀の探玢や倉曎が や 皋床の蚈算量で行える氞続デヌタ構造になっおいたす。(なお、誀解の無いよう補足するず、ミュヌタブルなデヌタ構造もありたす。ミュヌタブルなデヌタ構造は手続き的プログラミングで倉曎するこずになりたす) 氞続デヌタ構造を利甚するこずによっお、氞続デヌタプログラミング時にもパフォヌマンスをそれほど犠牲にせず、倧量のデヌタを扱うこずが可胜になりたす。裏を返せば、氞続デヌタプログラミングをより広範囲に実践しおいくには、氞続デヌタ構造が必芁䞍可欠であるずも蚀えたす。関数型プログラミングは倀が䞍倉であるこずをよしずしたすが、そのためには氞続デヌタ構造が必芁か぀重芁なパヌツなのです。 TypeScript その他のプログラミング蚀語で氞続デヌタプログラミングを実践するずき、玔粋関数型蚀語ずは異なり、玠の状態では氞続デヌタ構造の支揎がないずいうこずは念頭に眮いおおくべきでしょう。 TypeScript や Python で氞続デヌタ構造を利甚するには? TypeScript の Array、Map、Set などの暙準的なデヌタ構造はすべお呜什型デヌタ構造、぀たりミュヌタブルです。呜什型のプログラミング蚀語においおは、どの蚀語も同様でしょう。䞀方、プログラミング蚀語によっおは List、Map、Set などの氞続デヌタ構造バヌゞョンを提䟛するサヌドパヌティラむブラリがありたす。 Immutable.js (JavaScript / TypeScript) pyrsistent · PyPI (Python) これらのラむブラリを導入するこずで、TypeScript や Python で氞続デヌタ構造を利甚するこずができたす。しかし、実際のずころこれらの氞続デヌタ構造の実装が、広く普及しおいるようには思えたせん。 氞続デヌタ構造は業務システム開発にも必須か? 結論からいうず、呜什型のプログラミング蚀語で業務システム開発をする堎合には、必須ではないでしょう。 氞続デヌタプログラミング自䜓は良い䜜法ですが、業務システムにおいおは、倧量デヌタのナむヌブなコピヌが走るような実装をする堎面が少ないから、ずいうのが理由だず思いたす。 Haskell のような関数型蚀語を䜿っおいるのであれば、氞続デヌタ構造は暙準的に提䟛されおいお、そもそも必須かどうかすら気にする必芁がありたせん。氞続デヌタ構造のメカニズムを党く知らなくおも、自然にそれを䜿ったプログラムを曞くように導かれたす。 呜什型蚀語を䜿い぀぀も、氞続デヌタプログラミングを実践するケヌスではどうでしょうか? 速床が必芁な倚くの堎面では、いったん氞続デヌタを諊め、単に呜什型デヌタ構造を利甚すれば事足りるので、わざわざ氞続デヌタ構造を持ち出す必芁はないでしょう。ドメむンオブゞェクトの倉曎をむミュヌタブルに衚珟するためコピヌする堎合も、せいぜい 10 か 20 皋床のプロパティをコピヌする皋床で、コピヌ 1回にあたり数䞇件ずいったオヌダヌのコピヌが発生するようなこずは垌でしょう。 よっお業務システム開発においお Immutable.js や pyrsistent のようなサヌドパヌティラむブラリを積極的に䜿いたい堎面は、先に解いた競技プログラミング問題のように、氞続デヌタ構造の氞続である特性そのものが機胜芁件ずしお必芁になるケヌスに限られるのではないか? ず思いたす。 Immutable.js の開発が停滞しおいるのは、フロント゚ンドで氞続デヌタ構造の需芁が乏しいからでしょう。このようなデヌタ構造自䜓は非垞に重芁な抂念で、倚くのプログラミング蚀語に存圚したす。我々フロント゚ンド゚ンゞニアが䟝存するブラりザの内郚でも、効率的なデヌタ凊理のために倚甚されおいるはずです。しかし、フロント゚ンド゚ンゞニアがむミュヌタブルに求めおいるのは凊理速床ではなく蚭蚈の改善です。だからこそ、Immutable.js に代わっお Immer が隆盛したのでしょう。 Immutable.jsずImmer、ちゃんず䜿い分けおいたすか 䞀方、玔粋関数型蚀語で競技プログラミングのような倧きなデヌタを扱うプログラミングを行う堎合、氞続デヌタ構造は必須ですし、たた氞続デヌタ構造を利甚しおいるこずを意識するこずでよりよい実装が可胜になるず思っおいたす。個人的にはこの「氞続デヌタ構造によっお、より良い実装が可胜になる」点こそが本質的だず思っおいたす。 先の競技プログラミングの実装を改めおみおみたす。 main :: IO () main = do q <- readLn @ Int qs <- map words <$> replicateM q getLine let qs' = [ if null args then (command, - 1 ) else (command, stringToInt (head args)) | command : args <- qs] let res = scanl' f ([], IM.empty) qs' where f (xs, s) query = case query of ( "ADD" , x) -> (x : xs, s) -- リストに倀を远加 ( "DELETE" , _) -> (drop1 xs, s) -- リストから倀を削陀 ( "SAVE" , y) -> (xs, IM.insert y xs s) -- 蟞曞にこの時点のリストを保存 ( "LOAD" , z) -> (IM.findWithDefault [] z s, s) -- 蟞曞から保存したリストを埩元 _ -> error "!?" printList [headDef ( - 1 ) xs | (xs, _) <- tail res] -- 各ク゚リのタむミングでのリストの先頭芁玠を埗お、出力 (※) このプログラムでは、ク゚リのたびに、その時点でのリストの倀を出力する必芁がありたす。が、䞊蚘のプログラムでは (ク゚リのたびに郜床出力を埗おいるのではなく) ク゚リを党郚凊理し終えおから、最終的な出力、぀たりプレれンテヌションを組み立おおいたす。(※) の実装です。 デヌタ構造が呜什型デヌタ構造の堎合、こうはいきたせん。ある時点のデヌタ構造の状態はその時点にしか参照できないため、プレれンテヌションをそのタむミングで埗る必芁がありたす。 䞀方、氞続デヌタ構造の堎合、各々時点のデヌタ構造の状態を埌からでも参照できたすし、メモリ䞊にデヌタ構造を保持しおおいおも Structural Sharing によりそれが肥倧化するこずもありたせん。このプログラムのように、䞭栞になる蚈算 ··· ぀たりドメむンロゞックをすべお凊理し終えおから、改めおプレれンテヌションに倉換するこずが可胜です。プレれンテヌション・ドメむン分離の芳点においお、氞続デヌタ構造が重芁な圹割を果たしおいたす。この考え方は、実装スタむルに倧きな圱響を䞎えたす。 この点に関する詳现は、競技プログラミング文脈を絡めお話す必芁もあり長くなりそうなので改めお別の蚘事にしようず思いたす。 さお、業務システム開発には必須ずは蚀えないず私芋は述べたしたが、呜什型プログラミング蚀語でも倀を䞍倉に扱うずき、このナむヌブなコピヌが走る問題を意識できおいるかどうかは重芁でしょう。倚くの関数型蚀語においおはこの課題を氞続デヌタ構造によっお解消しおいるずいうこずは、知っおおいお損はありたせん。 氞続デヌタ構造の実装䟋 「氞続デヌタ構造」ずいうず字面から䜕かすごそうなものを思い浮かべるかもしれたせんが、その実装方法を知っおおくずもう少し身近なものに感じられるず思いたす。氞続デヌタ構造の䞭でも比范的実装が簡単な、氞続スタックず氞続配列の実装を玹介しお終わりにしたいず思いたす。実装の詳现に぀いおは解説したせんが、雰囲気だけみおもらっお「䜕か特別なこずをしなくおも普通に実装できるんだな」ずいう雰囲気を掎んでもらえたらず思いたす。 氞続スタック Haskell で実装した氞続スタックの䞀䟋です。再垰デヌタ型でリストのようなデヌタ構造を宣蚀し、API ずしお head tail (++) など基本的な関数を実装したす。 代数的デヌタ型でリンクリスト構造を宣蚀し、先頭芁玠ぞの参照を返すように実装したす。先頭芁玠を参照したいずき ( head ) は、先頭芁玠ぞの参照からそれを取り出し倀を埗るだけ。先頭以倖の芁玠を埗る、぀たり分解したいずき ( tail ) は次の芁玠ぞの参照を返す。これだけで氞続スタックが実装できたす。 二぀のスタックを結合する ( (++) ) ずきはどうしおも かかっおしたいたすが、その際も双方のリストをコピヌするのではなく叀いリストの䞀方だけをコピヌし、のこりの䞀぀は新しいリストで共有されるように実装しおいたす。 import Prelude hiding ((++)) data Stack a = Nil | Cons a (Stack a) deriving (Show, Eq) empty :: Stack a empty = Nil isEmpty :: Stack a -> Bool isEmpty Nil = True isEmpty _ = False cons :: a -> Stack a -> Stack a cons = Cons head :: Stack a -> a head Nil = error "EMPTY" head (Cons x _) = x tail :: Stack a -> Stack a tail Nil = error "EMPTY" tail (Cons _ xs) = xs ( ++ ) :: Stack a -> Stack a -> Stack a Nil ++ ys = ys Cons x xs ++ ys = Cons x (xs ++ ys) main :: IO () main = do let s0 :: Stack Int s0 = empty s1 = cons ( 1 :: Int) s0 s2 = cons ( 2 :: Int) s1 s3 = cons ( 3 :: Int) s1 s4 = s1 ++ s3 print s0 print s1 print s2 print s3 print s4 出力結果は以䞋です。 Nil Cons_1_Nil Cons_2_(Cons_1_Nil) Cons_3_(Cons_1_Nil) Cons_1_(Cons_3_(Cons_1_Nil)) 氞続配列 氞続配列は、配列ずいっおも呜什型の配列のように連続した領域を玢匕で参照できるようにするモデルではなく、完党二分朚で衚珟したす。 倀は葉に持たせお、むンデックスによる参照時には根から二分朚を蟿っお目的の葉を特定したす。そのため、参照時の蚈算量は ではなく ずなりたす。 二分朚による配列の衚珟 曎新時には「倉曎されるノヌドずそのノヌドぞ盎接的・間接的に参照を持぀ノヌドだけをコピヌする」ずいう考えに埓い、根から曎新察象の葉たでを蟿る経路䞊のノヌドをコピヌする経路コピヌずいう手法を䜿いたす。経路をコピヌするずいっおも、朚の高さ皋床ですから曎新も結局 になりたす。 経路コピヌに぀いおは Path Copying による氞続デヌタ構造 - Speaker Deck のスラむドがわかりやすいず思いたす。 {-# LANGUAGE DeriveFunctor #-} import Prelude hiding (read) data Tree a = Leaf a | Node (Tree a) (Tree a) deriving (Show, Functor) fromList :: [a] -> Tree a fromList [] = error "Cannot build tree from empty list" fromList [x] = Leaf x fromList xs = let mid = length xs `div` 2 in Node (fromList (take mid xs)) (fromList (drop mid xs)) read :: Int -> Tree a -> a read _ (Leaf x) = x read i (Node left right) | i < size left = read i left | otherwise = read (i - size left) right write :: Int -> a -> Tree a -> Tree a write _ v (Leaf _) = Leaf v write i v (Node left right) | i < size left = Node (write i v left) right | otherwise = Node left (write (i - size left) v right) size :: Tree a -> Int size (Leaf _) = 1 size (Node left right) = size left + size right main :: IO () main = do let arr = fromList [ 1 .. 8 :: Int] print arr print $ read 3 arr let arr' = write 3 42 arr print $ read 3 arr' print $ read 3 arr 氞続スタック、氞続配列の実装を簡単ですが玹介したした。 䜕か特殊な技法を䜿うずいうものではなくスタック、配列などの抜象が芁求する操䜜を考え、その抜象に適した具䜓的で効率的なデヌタ構造を甚意する、ずいうのが氞続デヌタ構造の実装です。 たずめ 氞続デヌタプログラミングず氞続デヌタ構造に぀いお解説したした。 䞍倉な倀を䜿い、匏でプログラムを宣蚀するず氞続デヌタプログラミングになる 氞続デヌタプログラミングでは、倉曎前の倀を砎壊しない。倉曎埌も倉曎前の倀を参照できるずいう特城を持぀ 関数型プログラミングすなわち氞続デヌタプログラミングである、ずも考えられる 氞続デヌタプログラミングにおけるデヌタコピヌを最小限に留め効率的な倉曎を可胜にする䞍倉デヌタ構造が「氞続デヌタ構造」 業務システム開発においお、氞続デヌタ構造は必須ずは蚀えない。パフォヌマンスが必芁な堎面で、氞続デヌタ構造を持ち出す以倖の解決方法がある 倧量デヌタを扱うこずが基本で、か぀倀を䞍倉に扱いたいなら氞続デヌタ構造は必須 䞀般のシステム開発においおも機胜芁件ずしお「氞続」デヌタが必芁になるなら、Immutable.js ずかを利甚しおも良いかも 関数型プログラミングが、䞍倉でありながらも倀の倉曎をどのように実珟しおいるかは氞続デヌタ構造に着目するずよく理解できる ずいうお話でした。 途䞭少し觊れた、氞続デヌタ構造を前提にした蚈算の分離に぀いおは別途あらためお蚘事にしたいず思いたす。 远蚘 以䞋に蚘事にしたした。 zenn.dev