組み分け帽子
データを考える
生徒 : {名前: string, 希望講義: [講義.名前(string)]}
講義 : {名前: string, 募集上限: number, 参加生徒: [生徒.名前(string)]}
生徒から講義の名前以外、講義から生徒の名前以外を見ないのでとりあえず名前(string)だけにした
希望講義は希望順に入ってる
希望講義[0] = 第1希望, 希望講義[1] = 第2希望, ...
データの流れを考える
Data :: {[生徒リスト], [講義リスト]}を組分け用関数(grouping)に通し続けるようにした
grouping :: 講義.名前(string) -> 希望順(number) -> Data -> Data
1. Data.生徒リスト内の生徒の内、引数の講義.名前を引数の希望順に希望している生徒を抽出
2. Data.講義リスト内の講義の内、引数の講義.名前を持つ講義を抽出
3. 2.の講義の既存の参加生徒, 募集上限を元に参加可能な生徒数を算出
4. 1. で抽出した生徒が3.で求めた生徒数を上回っていた場合切り捨て
5. 2.の講義の参加生徒に抽出した生徒の名前を追加
6. Data.生徒リストから抽出した生徒を除外
7. Data = {生徒リスト: (6.で更新した生徒リスト), 講義リスト: (5. で更新した講義リスト)}を返却
こういう関数を下記のように一生回し続ける(実際は下記の形ではないが、やってることは似たようなもん)
なんで下記の形ではない(下記の形にできない)かは後述
code:typescript
let Data = {students, subjects}
Data = grouping('講義1')(1)(Data)
Data = grouping('講義2')(1)(Data)
Data = grouping('講義3')(1)(Data)
Data = grouping('講義1')(2)(Data)
...
こうやって行くと最終的に、
Data.studentsには組分けできなかった生徒
Data.subjectsには講義名と、参加する生徒の名前
が残るようになる
grouping関数をさらに細かい関数に分ける
コアの処理は思いついたので、細かいパーツから組み立てて、最終的にパーツを合成してgrouping関数を組み上げる
とりあえず下記の関数を作った
findSubject :: 講義名(string) -> 講義リスト -> 講義
指定の名称の講義を講義リストから抽出する
extractJoinStudents :: 講義名(string) -> 希望順(number) -> 生徒リスト -> 生徒リスト
生徒リストから指定の講義を第n希望として希望している生徒を抽出し、新しいリストを返却する
cutStudentsOverCapacity :: 参加可能人数(number) -> 生徒リスト -> 生徒リスト
生徒リストの内参加可能人数を上回った生徒を除外した新しいリストを返却する
removeJoinedStudents :: 生徒リスト -> 生徒リスト -> 生徒リスト
第1引数の生徒リストから、第2引数の生徒リストを除外した新しいリストを返却する
setStudentsToSubject :: 生徒リスト -> 講義 -> 講義
講義.参加生徒に生徒リスト(の生徒名)を追加して返却
パーツを組み合わせてgrouping関数を作る
code:typescript
const grouping = (subjectName: string) => (choice: number) => (data: Data): Data => {
...
}
と言う形になる訳だが…導出したい項目は
1. 指定の講義に参加する生徒のリスト(joinStudents)
data.生徒リストに対してextractJoinedStudents, cutStudentsOverCapacityを順番に適用すればok
2. joinStudentsを参加生徒に追加した講義(subjects)
data.講義リストを走査してsubjectNameを持つ講義に対しsetStudentsToSubjectを適用
どちらかと言うとdata.講義リストの内subjectNameを持つ講義を上記の形に更新……だな
3. joinStudentsが除外されたdata.生徒リスト(students)
removeJoinedStudents(data.生徒リスト)(joinStudents)で導出可能
最終的に{students(3.で導出したやつ), subjects(2.で導出したやつ)}を新しいDataとして返却すればok
実際に複数回適用させる
何が難しいかと言うと、講義数(第n希望まである)が可変であるということ
実際にどうやったかと言うと……
1. 講義リストを講義名のみに変換後、各々の講義名にgroupingを適用
[grouping('講義1'), grouping('講義2'), ... , grouping('講義n')]のような配列になる
2. 1〜n希望の連番のリストを作る(Data.生徒.希望講義の長さ 或いは講義リストの長さを取ってあげればok)
[1, 2, 3, ... , n]
3. 連番のリストの各要素に対し、「1. のリストの各要素に指定した引数を適用する」関数を適用する
code:typescript
const arr = [
...
]
みたいな配列ができる
4. 配列をflatにしてArray.reduceで畳み込む
reduceする為に二次元の配列になっているarrを一次元にする arr.flat()するとこうなる
code:typescript
const arr = [
grouping('講義1')(1), grouping('講義2')(1), ... , grouping('講義n')(1),
grouping('講義1')(2), grouping('講義2')(2), ... , grouping('講義n')(2),
grouping('講義1')(3), grouping('講義2')(3), ... , grouping('講義n')(3),
...
]
const grouping_ = arr.flat().reduce((acc, current) => compose(current, acc))
これでgrouping_ :: Data -> Dataという関数が出来上がる
5. 最後にgrouping_にData型のデータを渡してやれば……
Data.studentsには組分けできなかった生徒
Data.subjectsには講義名と、参加する生徒の名前
のデータとして返却される!
6. あとは好きな場所に返却されたデータを出力すればok
魔法みたいだろう?
これのおかげでlet data = {students, subjects}への再代入がいらなくなった
仕組みとしては、grouping('講義1')(1), grouping('講義2')(1)は共にData型を受け取ってData型を返すので、grouping('講義1')(1)(grouping('講義2')(1)(Data))みたいに入れ子にできる
grouping('講義1')(1)(grouping('講義2')(1)(Data))
compose(grouping('講義2')(1), grouping('講義1')(1))(Data)
上記2つは等価