「わかる!ドメイン駆動設計〜もちこちゃんの大冒険〜」を読んだ
わかる!ドメイン駆動設計〜もちこちゃんの大冒険〜を読む。 社内のアーキテクチャや設計はDDD的な要素が入っているので知らないままではいられない。
DDD
- Domain Driven Development(ドメイン駆動設計)
DDDでやること
- 開発者とドメインエキスパートがドメインモデルを作る
DDDのメリット
- ドメインエキスパートと開発者を同じ土俵に乗せることで、プログラマーの視点だけでなく業務側の視点も入れられる
- ソフトウェアを理解している人を一部だけという状況をなくせる
- ドメインエキスパートとソフトウェア開発者、そしてソフトウェアそのものとの一切の通訳が不要になる
- 設計がコードであり、コードが設計になる
- 知識の継承、ビジネス要件に伴う機能変更にすばやく対応できる
DDDの要素
戦略的設計
と戦術的設計
に分類される- 戦術的設計だけでもDDDの恩恵は受けられるが両者が組み合わさったときのような大きな利益は生まれない
ユビキタス言語
- ユビキタス言語とは、ドメインエキスパートやソフトウェア開発者を含めたチーム全体で作り上げる
共通言語
のこと - ユビキタス言語の見つけ方
- ユビキタス言語
- 一度決めたらそれっきりではなく、ユビキタス言語も育っていく
ドメインモデル
- ドメインを反映したモデル
- 名前・関係性・振る舞いなどが表現されていて、チームメンバー全員が同じものを思い浮かべるもの
- DDDでは設計がコードであり、コードが設計である
- ドメイン、ドメインモデル、ソフトウェアモデル、オブジェクトモデルの関係性
ドメインモデル貧血症
ドメインとは
- ドメインとは業務に関連した領域
- ソフトウェアには適用する業務領域があるが、個々の機能や構成を見ていくと複数の
サブドメイン
に切り分けられる
境界づけられたコンテキスト
- ドメインモデルがどこに属するかを表すもの
- 新規のプロジェクトなどは1つのサブドメインに対して1つの境界づけられたコンテキストになるが、そうではないケースもある
- 境界づけられたコンテキストのサイズを間違える原因として多いもの
- 言語的な境界ではなく技術的な境界を考えてしまっている
- チームの開発要員へのタスクの割当を考慮してしまっている
コンテキストマップ
- 境界づけられたコンテキスト間の関係性を描いたもの
- 注意点
- 境界つけられたコンテキスト間には下記のような関係がありうる
感想
- DDDについて今まで実装の話を少し読んでみたりしてピンとこなかったのがとても身近になったしDDDを本格的に学ぶモチベーションが上がった!
- 前の職場では、エンジニアと非エンジニアが違う言葉使っているというのはあったな、と思った。それが原因で意思疎通に支障が生じることもあったと記憶している。ユビキタス言語を育てる、ということを取り入れるだけでもいいコミュニケーションにつながりそう
- まだ先は長そうなので頑張ろうと思う
- 次は下記資料の1番上のドメイン駆動設計の要約を読む
他のドメイン駆動設計のよさげな資料
- ドメイン駆動設計の要約
- 要約が無料ダウンロードできる!!
- この本を読んだ後に読むといいよって教えてもらった
- エリック・エヴァンスのドメイン駆動設計
- 原典
- 難しいけど読む度に発見がある名著だから取り組むべきらしいので
- 実践ドメイン駆動設計
- エリック・エヴァンスより平易らしくこれもマストらしい
- little hands' lab
- ドメイン駆動設計を広めている方のブログ。コードもあってわかりやすい
プログラミングの基礎を読んだ
プログラミングの基礎を読んだのでまとめ
はじめに
関数型言語を仕事でも個人でも使ったことがなかったので何かしら触ってみたいと思っていたところ知人に進められたのがきっかけで読み始めた。
情報系出身ではなくコンピュータサイエンスの基礎にあやふやな部分が少なくない自分にはとても良かった。
学んだこととプラスアルファ
この本から学んだことのまとめ
自分が学びになったことのまとめなので網羅的な要約ではないことに注意
変数
- Ocamlの変数は基本的には一度定義すると後から変更することができない
- 他言語でいうところのconstのようなものだろうか
参照透過性
という性質(後述)
型推論と型チェック
- 型を定義するとその型が何の型を受け取って何の型を返すのかを、関数内部の演算子などから推測することを
型推論
という - また定義された関数への引数の型が推論した型と合致しているかをチェックすることを
型チェック
という
# let add a b = a + b ;; (* int2つを受け取り、int1つを受け取る関数であると推論している *) val add : int -> int -> int = <fun> # add "a" "b" ;; (* int型が期待されているのにstring型が渡されたというエラーを発生させている *) Error: This expression has type string but an expression was expected of type int
パターンマッチ
- 組(複数の値をひとまとめにしたもの)やレコード(ハッシュのようなもの)、型といった構造データからデータの中身を取り出す方法
- 入力が構造データの場合は行う
- 下記が典型的な形で、
match
とwith
に挟まれた式を実行し、その結果をパターンと比較する- パターンは復数列挙できる
match 式 with パターン -> 式 | パターン -> 式
レコード
- 定義された型のフィールドを省略して、レコードを作ることはできない
- パターンマッチでパターン変数に束縛する時は省略できる
- 型同士でフィールドの重複は許可されない
意味のあるかたまりに対して一つの型を定義するのが望ましいプログラミングスタイル
- 他言語でも同様だと思う。
リスト定義
2つの定義によって帰納的に定められている。
自分自身を使って定義されたデータ型を再帰的なデータ型
、あるいは自己参照をするデータ型
と呼ぶ
- 空リスト
[]
はリストである - firstが要素、restがリストなら
first::rest
もリストである(自分自身を定義に使っている)
上記の定義よりリストのパターンマッチは空かそれ以外のケースの2通りで表すことができるようだ
match リスト with [] -> 式 | first :: rest -> 式
またリストは内部的にはLinked List
になっているらしい
なので、リストを再帰的にパターンマッチしていくことは連結リストをheadから次のnodeを見ていくことに等しい
自然数定義
リストと同じように2つの定義かつ帰納的に定めることができる
ダイクストラのアルゴリズム
- アルゴリズム関連はプログラミングコンテスト攻略のためのアルゴリズムとデータ構造でまとめてやった
- 関数型でもう一度やり直したら楽しそうだと思った(TODO)
高階関数
- Ocaml の関数は
first-class
の値 -> 他の値と同じように扱うことができる- 関数が引数にも返り値にもなることができる
- 関数を引数として受け取る関数のことを
高階関数
と呼ぶ- OcamlのList.map、List.filterなどはそれにあたる
多相型と多相関数
- 'a, 'b といった型は
型変数
と呼ばれ、どのような型でもよいことを表す- プログラム実行時に具体的な型に置き換わる
- どのような型でもよいという性質のことを
多相性(polymorphism)
と呼ぶ - 型変数を含むような型のことを
多相型
、多相型を持つ関数のことを多相関数
と呼ぶ <-> 単相関数
infix関数とprefix関数
- prefix関数
- 関数を引数の先頭に書き、引数を後に並べる関数。普通の関数
- infix関数
+
演算子など。関数を引数の間に置く- このinfix関数の中にはprefix関数に変換できるものがあり、それを使うと下記のようにプログラムを簡潔にできるかも
(* リスト lst の関数の和を求める関数 *) (* sum : int list -> int *) let sum lst = List.fold_right (+) lst 0
リストの性質
- 似たような性質のデータの集めたもの
- mapやfilter、fold_rightなどはそのような同種のデータを一括して扱うという意味で、リスト処理の本質を捉えていると言える
- map : リストの全要素に対して同じ処理を施す
- filter : リストの中から特定の要件を満たす要素を抽出する
- fold_right : リストの各要素をまとめあげる
- 再帰的にプログラムすることで関数名を使わずに実行できるが、リストの各要素をバラバラではなく1つのデータとして扱いたい
- 例えば下記の場合、 (enumerate n) は
1~nまでのリスト
、(divisor n) はnの約数
という同種のデータの集まりになる
- 例えば下記の場合、 (enumerate n) は
- mapやfilter、fold_rightなどはそのような同種のデータを一括して扱うという意味で、リスト処理の本質を捉えていると言える
lee rec enumerate n = if n = 0 then [] else n :: enumerate (n - 1) let divisor n = List.filter (fun x -> n mod x = 0) (enumerate n)
再帰関数
- データ構造にしたがった再帰
[] | first :: rest で分けられるようなデータ構造
- メリット
- 簡単に関数を作ることができる
- 停止性が明らか
- メリット
- 一般的な再帰(データ構造に従わない再帰)
- 下記を考慮する必要がある
- 自明に答えが出るのはどのようなケースか
- より小さな部分問題はどのようにしたら作れるか
- 再帰呼出しの結果から全体の結果がどのように得られるか
- 下記を考慮する必要がある
停止性の判定
一般の再帰は構造に従った再帰と違って、再帰呼出しの停止性が自明ではないので下記2点を確認する
- 再帰呼び出しを行う際の部分問題が何らかの意味でもとの問題よりも簡単になっていること
- それを繰り返すと有限回で自明なケースに帰着されること
一般的な再帰
- 自明に答えが出るケースとそれ以外のケースについて考える
- その上で下記4つのことを考える
- 自明に答えが出るケースはどのような場合か
- その場合の答えはなにか
- 部分問題はどのように作ることができるか(1つとは限らない)
- 部分問題の結果から全体の結果を得るにはどのようにすればよいか
情報の蓄積
- 欠落している情報を補うための引数
アキュムレータ
バリアント型
まずは下記の例を参照
# type color_t = Blue | Red | Black ;; - : type color_t = Blue | Red | Black # Black ;; - : color_t = Black
- Blue、 Red、Black を
構成子
として、color_t という型を宣言している - このcolor_tが
バリアント型
木構造
type tree_t = Empty | Leaf of int | Node of tree_t * int * tree_t # 使う時はこんな感じ # tree1 = Empty # tree2 = Leaf (1) # tree3 = Node (Empty, 2, Node (Emply, 5, Leaf(9)))
type ('a * 'b) association_tree_t = Empty | Leaf of 'a * 'b | Node of ('a * 'b) association_tree_t * 'a * 'b * ('a * 'b) association_tree_t # 使う時 # tree1 = Empty # tree2 = Leaf ("nekootoko3", 100) # tree3 = Node (Empty, ("nekootoko3", 100), Node (Leaf ("pokemon", 100), ("occhan", 1500), Empty))
全通りを尽くしていない場合の対処
- 例外の場合に
infinity
を返さなければならないケースなどが存在すると思う- 例えばそれが空のリストを渡された場合に発生するとしたら、そもそも引数を2つ受け取るようにすることで解決できる
- 引数の1つ目はリストには本来リストに含まれる値を渡し、その値とリストとで処理を行う。空のリストの場合には1つ目の値をそのまま返す
- 例えばそれが空のリストを渡された場合に発生するとしたら、そもそも引数を2つ受け取るようにすることで解決できる
- プログラムの構造を見直すことで網羅性高くうまい具合に例外も処理できる場合がある
オプション型
- Ocamlに下記のような定義で組み込まれている
- 値が存在しない場合の
None
と何かしらが存在しているSome ('a)
を区別することができる- これによって該当値が見つからない場合は0を返すなど、特殊な返り値を使う必要が明確に存在しないことを表すことができる
type 'a option = None | Some of 'a
例外と例外処理
- 他言語でもよくあるような形だと思う
- raise
定義されたエラー型
で例外を発生させられる exception 定義したいエラー型 (of int)
でエラー型の定義もできる- 下記のように例外の補足・処理を記述できる
try 式 with エラー型 -> 処理 | エラー型 -> 処理
モジュール
- 通常のモジュールと変わらない
- 意味的にまとまりのある定義を1つにまとめる
- 外部に公開するインターフェースは
シグネチャ(signature)
と呼ばれる - シグネチャとモジュールの実体は例えば下記のように宣言できる
module モジュールの名前 : sig シグネチャの本体 ex) type ('a, 'b) t ex) val empty : ('a, 'b) t ex) val search : ('a, 'b) t -> 'a -> 'b end = struct モジュールの本体 ex) ここでは上で外部宣言されている3つを実装する必要がある 1. 連想を持つ`t`という型 2. `t`という型の構成子からなるemptyという変数 3. `t`という型、とtのキーとなっている型を受け取りtの値を返すsearchという関数 end
- 他言語のインターフェースと同じようなものだと思う
副作用と参照透過性
- 値の受け渡し以外を行っている関数を
副作用を持つ関数
と呼ぶ- 例えば文字の表示や渡された変数に代入して値を書き換えることがなどがそれにあたる
参照透過性
とは一度定義された変数がその後変更されないという性質のことを指す- この性質によりプログラムの途中や関数内で変数が書き換えられないことが保証された上で処理を書くことができるのが嬉しい
- Ocamlのint型やリストなどは参照透過性を持っている
- 参照型(ref)や配列(array)は参照透過性(referrence tranceparrency)を持たない型であり、データが不変であることが保証されない
参照透過性の喪失が問題を引き起こすのはモジュールのシグネチャ(インターフェース)だけが公開されており、破壊的変更を行うように見えない時など
下記例は極端だが、関数の利用者は配列要素の合計値を知りたいだけだが、実際には配列の各要素にそれまでの合計値を入れてしまっている
この配列を関数に渡す前の状態と思って利用すると思わぬ結果を引き起こす
(* 何の型を受け取って何を返すかだけが公開されている) val add : int array -> int (* 実際の実装 *) let add arr = let sum = ref 0 in for i = 0 to (Array.length arr) - 1 do sum := !sum + arr.(i); arr.(i) <- !sum done; !sum (* 実際に配列を定義して渡してみると、、、) # let arr = [|1; 2; 3; 4; 5|] ;; val arr : int array = [|1; 2; 3; 4; 5|] # add arr ;; - : int 15 # arr ;; - : int array = [|1; 3; 6; 10; 15|] (* 中身が書き換わっている *)
感想
非常によい本だと思った。
プログラミング初学者向けかと言われると怪しいが、少しプログラミングに慣れてきてプログラミングの新たなパラダイムを学びたい、
基礎的な概念をちゃんと身につけたいといったニーズには応えられる本なのではないかと思う。
丁寧に解説されているし、練習問題も多く入っているので身につけられることは少なくない。
オプション型って素晴らしいなぁと思ったり、よいコードを書くために必要なことも学べたんじゃないかと思う。
ただ関数型言語について分かったかと言われるとどうだろう。
それに関しては実際に自分でより多くのプログララムを関数型言語で書くことでその良し悪しを実感したり、
同じく関数型言語の別言語を学んでみることで関数型言語というパラダイムを共通項をまとめてみたりする経験が必要な気がする。
というわけで関数型言語を巡る冒険はしばらく続けていきたい。
さしあたり次はoz/mozartやる
protocol buffers, gRPC周りの整理
protocol buffers, gRPC周りの整理
この記事に書くこと
書くこと
- gRPCの簡単な説明
- protocol buffersの簡単な説明
- 上記のサンプルコード(公式ドキュメントとほとんど同じ)
最初にざっくりと
gRPC概要
- クライアントが別環境のサーバーのメソッドをローカルのメソッド呼び出しのように行える仕組み
protocol buffers
で、型、メソッド、メソッドがどの型を受け取り、どの型を返すかを定義する- 型定義はjsonでも問題ないが通常はprotocol buffersを使うことを想定している
- http/2.0 をベースとしている
protocol buffers 概要
- 構造化されたデータを自動的に読み書きするための機構
- XMLみたいなものだが、XMLより可読性高く各言語の構造体へのエンコード、デコードも早い。
protoc
- .protoファイル用コンパイラ
- protoファイルの定義したデータ構造から任意の言語でのデータアクセス用クラスを生成する
サンプルコード
- 下記がprotocol buffersの例。
service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} rpc SayGoodBye (GoodByeRequest) returns (GoodByeReply) {} } message HelloRequest { string name = 1; int32 count = 2; } message HelloReply { string message = 1; int32 count = 2; } message GoodByeRequest { string name = 1; } message GoodByeReply { string message = 1; }
- 詳細はドキュメントを読んでもらうとして、簡単な概要。
- service
Greeter
は、rpcSayHello
メソッドと rpcSayGoodBye
メソッドを持っている SayHello
メソッドは messageHelloRequest
型を受け取り、 messageHelloReply
型を返すHello
メソッドは messageHelloRequest
型を受け取り、messageHelloReply
型を返す- HelloRequest型は、name(string), count(int32)を持つ型
- HelloReply型は、message(string), count(int32)を持つ型
- GoodByeRequest、GoodByeReplyに関しても同様。
- service
この.protoファイルを眺めると、サービスがどんなメソッドを持っていて、何を受け取って何を返すかがすぐに分かる!!
- protocでgolangのコードを生成すると、下記のようなコードが生成される(一部抜粋)
// ----------------------------------- // 構造体関連 // ----------------------------------- type GoodByeRequest struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GoodByeRequest) Reset() { *m = GoodByeRequest{} } func (m *GoodByeRequest) String() string { return proto.CompactTextString(m) } func (*GoodByeRequest) ProtoMessage() {} func (*GoodByeRequest) Descriptor() ([]byte, []int) { return fileDescriptor_17b8c58d586b62f2, []int{2} } func (m *GoodByeRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GoodByeRequest.Unmarshal(m, b) } func (m *GoodByeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_GoodByeRequest.Marshal(b, m, deterministic) } func (m *GoodByeRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GoodByeRequest.Merge(m, src) } func (m *GoodByeRequest) XXX_Size() int { return xxx_messageInfo_GoodByeRequest.Size(m) } func (m *GoodByeRequest) XXX_DiscardUnknown() { xxx_messageInfo_GoodByeRequest.DiscardUnknown(m) } var xxx_messageInfo_GoodByeRequest proto.InternalMessageInfo func (m *GoodByeRequest) GetName() string { if m != nil { return m.Name } return "" } // ----------------------------------- // クライアント関連 // ----------------------------------- // GreeterClient is the client API for Greeter service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type GreeterClient interface { // Sends a greeting SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) // Sends a greeting again SayGoodBye(ctx context.Context, in *GoodByeRequest, opts ...grpc.CallOption) (*GoodByeReply, error) } type greeterClient struct { cc *grpc.ClientConn } func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { out := new(HelloReply) err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *greeterClient) SayGoodBye(ctx context.Context, in *GoodByeRequest, opts ...grpc.CallOption) (*GoodByeReply, error) { out := new(GoodByeReply) err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayGoodBye", in, out, opts...) if err != nil { return nil, err } return out, nil } // ----------------------------------- // サーバー関連 // ----------------------------------- // GreeterServer is the server API for Greeter service. type GreeterServer interface { // Sends a greeting SayHello(context.Context, *HelloRequest) (*HelloReply, error) // Sends a greeting again SayGoodBye(context.Context, *GoodByeRequest) (*GoodByeReply, error) } func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { s.RegisterService(&_Greeter_serviceDesc, srv) }
- サーバー側では、SayHello, SayGoodBye を実装したGreeterServerを実装する。
- クライアント側では、GreeterClientを通じてSayHelloRequest、SayGoodByeを必要に応じて呼び出す
次に書きたいこと
procol buffers で型に値が入っていない場合とデフォルト値を区別する
- protocol buffers では型のフィールドに値が入っていない場合、デフォルト値が使われてしまう
- Stringだと""、int32だと0といった具合にデフォルト値が入る。
- 値が無い場合とデフォルト値をクライアントが入れてきている場合を区別したい場合があるはずだ。
- 例えば Person型に
int32 age
を定義した時、age が不明だからageフィールドを空にして送るときがあるはず。一方で age = 0 を送ることも想定されるが、unknownと0は明確に区別したい、など。
- 例えば Person型に
- 上記のような時に、デフォルト型をラップしてnilを使えるようにする。
grpc-gateway
- 何らかの理由でクライアント-サーバー間で直接gRPCを使えない時があるかもしれない。
- だけどprotocol buffersはとても分かりやすいインターフェースなので使いたい。
- grpc-gatewayを使うと、サーバーの前段にjson API を gRPC に変換してくれるリバースプロキシをprotoco buffersの定義から生成してくれるらしい。
参考
rails開発3週間で学んだことなど
rails開発を始めて早いものでもう3週間も経った。 最初の開発も一段落しつつあるので何かメモを残しておこうかなと。 rails以外でも学んだことは多いのでそれも含めてのメモ。
コードリーディング編
使っているライブラリやモジュールを全てgrepで調べることはできない
- 例えばgolangでは
- 使うモジュールやライブラリは最初に宣言するので、そこを見ると外部パッケージを使っているのかそれとも内部に実装されているものを使っているのかが分かる
import "github.com/gin-gonic/gin" // 外部パッケージ import "nekootoko3/app/server/meshi" // 内部実装
- railsではスクリプト内で明示的に使うパッケージが明示されていないことが多い
- 親クラスでの実装、あるいはincludeしていればすぐに辿ることができる。
- しかしgemとしてinstallしているパッケージに関しては突然何の前触れもなく現れてこいつはだれだ!?となる
rails console の登場
- rails開発はデバッグも含め、このコンソール抜きでは進めることができないと言っても過言ではない
- pryが入っていて色々めちゃめちゃ便利にできるようになっている。
- dotenvなどを使って環境変数から変数の中身を取り込むようになっている変数の表示ができる
- (コンソール上で) >>
$
突然出てきたモジュール名- 実装元のファイルとその実装内容を表示してくれる!
- 実際にモデルやメソッドを動かして何がアウトプットされるのか確認できる
- (コンソール上で) >>
cd
オブジェクト- オブジェクトに入ることができる!
- 下記のlsを行うと使える
- (コンソール上で) >>
ls
(突然出てきたモジュール名、何も指定しなくてもよい)
>> Delayed::Worker::VARIABLE => 3 >> $ Delayed::Worker::VARIABLE いろいろ出てくる
- rails のコードリーディングはこれを活用しよう
デバッグ
- railsではメタプログラミングなるものがフル活用されており、動的にロジックが生成されるのでタイポがあっても普通にサーバーが立ち上がる(静的解析がされていない?)
- なので、
rails console
でコンソールを立ち上げて、該当のパスへのリクエスト、メソッドの実行などを行って確かめる。
コーディングのTIPS
多値の受け取り
- 関数からメソッドが2つ以上返ってくる時、受け取る変数が1つの場合、暗黙的に配列として受け取る。スカラー値として受け取りたい、ただし、2つ目の値はいらないという場合は下記のように `_` を使い、いらないけどとりあえず受け取っとくみたいなことをやるとよい。
a = return_two_values_method # a = [var1, var2] a, _ = return_two_values_method # a = var1, var2
返り値にハッシュではなく構造体を返す
return nil 明示的に書かなくてもよい
- ruby は最後に評価された値が返される
- 評価される値が何もない場合には、
nil
が返されるのでreturn
だけでもnil
が返される。
参考
プロを目指す人のためのRuby入門を読んだ
10月から転職してRubyも書く人になったので、プロを目指す人のためのRuby入門を読んでみた。 この流れで勉強すればよかったな、っていう振り返りも最後に合わせて記載。
この本をやる前の状態
- Ruby未経験からRubyとGoの会社に転職(一部Rust、C++、Pythonも動いている)
- 今までやったことがあった言語
- つまりオブジェクト指向を実務で使っていない(!)
- 入社前にRailsチュートリアルをやっていた。Rubyの勉強はしていない
読んでどうだったか
- ちょっとはプログラミング経験あって本気でRuby習得目指す人向けとあるように、初歩の初歩的な内容はほぼない。
- 本全体に渡ってサンプルコードが多く、説明もわかりやすかった。
- とはいえ、「第10章 yieldとProcを理解する」はわからなかった部分もあった。
- よく分からなかった部分もなんとなくそういうものがあるのだな、とは分かったので業務の中で直面した時に戻ってくればよいかな。
- Railsチュートリアルを読む前にやるべきだったな、と強く思う
- 他の本を読んでいないので比較はできないが、自分には優しすぎず難しすぎずなちょうどいいレベル感だと感じた。
- Railsやっていてよく分からないなぁ、というところも分かりやすく書いてあった印象。実務に戻ってみないと効果のほどは分からんが、、、
Rails学習のオススメな道筋
プログラミング経験1年以上を想定。 初心者目な人はProgateを先にガッツリやってみてから下記ステップへ。
- この本(プロを目指す人のためのRuby入門)をざっとやる。そんな時間かけなくてもいい。
- Railsチュートリアルをやる。ちゃんとやる。
- Rubyに関して分からないところが出てきたら、この本に立ち戻る
- Railsに関して分からないところが出てきたら、railsガイドを見よう。特にActiveRecordとかは重点的に見るといいと思う。
道筋と書いたけどこの2ステップになるかな〜
python3でプロコンのためのデータ構造とアルゴリズム
プログラミングコンテスト攻略のためのアルゴリズムとデータ構造の基礎編までを終えた。
アリ本が有名だが、こちらの方が評判がよくまとまっていると感じた。
新しい言語でやりたかったというのもあって、python3で書いている。
githubにコードと簡単なコメントあり。
なぜデータ構造とアルゴリズムの勉強を始めたのか
- 自分は文系出身だが情報工学を専攻していたエンジニアがベースとして持っている知識は身につけようと考えたから。
- インタプリタの実装をしてみたときや、広告配信サーバ周りの会話でちょっとしたアルゴリズムの話が出ることがあり、エンジニアのベースだと感じたから。
やってみて何かいいことあった?
- コードやライブラリの計算量を少し意識できるようになった
- 木の巡回やヒープ、動的計画法など、今までなんとなく見たようなものが体系だって学べた
- この問題はこんな綺麗に解決できるのか!という感動があるw
コンピュータサイエンスおもしろい!
ハノイの塔を実装する
前提
- 3つの杭を3つの配列(orig, tmp, to)で表現する
- 各配列をスタックとみなし行える操作はpushとpopのみ
- 円盤1つを配列内のintとして表現。大きな円盤は大きな数字で
- 例えば、3段のハノイの塔は
[3,2,1]
といった形で表現する
- 例えば、3段のハノイの塔は
目標
- n段のorigをtoに移し替えること
まずは
- 1段のとき、2段のとき、3段のときを考えてその法則性を探る
1段
- orig (1)-> to
orig = [] tmp = [] to = [1]
2段
- orig (1)-> tmp
# 1段と同じ操作をorig -> tmp orig = [2] tmp = [1] to = []
- orig (2)-> to
# 1段と同じ操作 orig = [] tmp = [1] to = [2]
- tmp (1)-> to
# 1段と同じ操作をtmp -> to orig = [] tmp = [] to = [2,1]
3段
- orig (1)-> to
- orig (2)-> tmp
- to (1)-> tmp
# 2段のときと同じ操作を orig -> tmp で実行 orig = [3] tmp = [2,1] to = []
- orig (3)-> to
# 1段のときと同じ操作 orig = [] tmp = [2,1] to = [3]
- tmp (1)-> orig
- tmp (2)-> to
- orig (1)-> to
# 2段目のときと同じ操作を tmp -> to で実行 orig = [] tmp = [] to = [3,2,1]
ちょっと多いけど4段目
- orig (1)-> tmp
- orig (2)-> to
- tmp (1)-> to
- orig (3)-> tmp
- to (1)-> orig
- to (2)-> tmp
- orig (1)-> tmp
# 3段のときと同じ操作を orig -> tmp で実行 orig = [4] tmp = [3,2,1] to = []
- orig (4)-> to
# 1段のときの操作 orig = [] tmp = [3,2,1] to = [4]
- tmp (1)-> to
- tmp (2)-> orig
- to (1)-> tmp
- tmp (3)-> to
- orig (1)-> tmp
- orig (2)-> to
- tmp (1)-> to
# 3段のときと同じ操作を tmp -> to で実行 orig = [] tmp = [] to = [4,3,2,1]
上記の試行から3つの手順に分けられる
N段のハノイの場合
- N-1段をorig->tmpに移動
- 1番下(N段目)をorig->toに移動
- N-1段をtmp->toに移動
手順1と3を再帰的に行うことで
コードにすると(python)
動かした回数を出したい場合は、実際に動かす操作を行なっている箇所、つまりto.appendの前後どちらかでカウントをインクリメントすればよい
def move(n, orig, to, tmp): if n == 0: return # n-1段をorigからtmpに移動する move(n-1, orig, tmp, to, cnt) # n段目をtoに移動する to.append(orig.pop()) # n-1段をtmpからtoに移動する move(n-1, tmp, to, orig, cnt) import sys # 引数でハノイの塔の高さを指定する if len(sys.argv) != 2: exit N = int(sys.argv[1]) # N段の塔、空の塔を配列で擬似的に表す orig = [i for i in range(N, 0, -1)] tmp = [] to = [] # origの塔をtoに動かす move(N, orig, to, tmp)