Reactの状態管理ライブラリRecoilを使ってみる
フロントエンドは、よくVueを使っているプロジェクトにいることが多いのですが、ここ最近は個人の時間でReactを勉強していました。状態管理ライブラリを探していたところRecoilというものがあることを知り、少し触れてみました。
この記事ではAtomの使い方について、簡単に書いておこうかと思います。
https://recoiljs.org/docs/introduction/core-concepts#atoms
環境
viteを使いプロジェクトを作成します。React + TypeScript のテンプレートを指定しています。
npm create vite@latest recoil-my-playground -- --template react-ts
ディレクトリに移動して、Recoilをインストールします。
npm install recoil
package.json の中身は次のとおりです。これを書いている時点でのRecoilのバージョンは、0.7.7
でした。
{ "name": "recoil-my-playground", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", "recoil": "^0.7.7" }, "devDependencies": { "@eslint/js": "^9.8.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "eslint": "^9.8.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", "typescript": "^5.5.3", "typescript-eslint": "^8.0.0", "vite": "^5.4.0" } }
atom を定義する
atomは状態の最小限単位です(名前からも最小単位であることが想像しやすいですね)。Recoilではatomを定義し、Reactコンポーネントがatomの値を読み込み/書き込みすることができるようになっています。
atomは次のように定義できます。
import { atom } from "recoil"; export const helloTypographyState = atom<string>({ key: "helloTypographyState", default: "this is read from recoil atom", });
ここでは、helloTypographyState
を定義しました。このatomはstring型の値を保持し、デフォルトが this is read from recoil atom
としています。Reactコンポーネントはこの値にアクセスすることができます。
atom を読み込む
atomを読み込むにはuseRecoilValue
を使います。useState
と同じ感じです。
上記のatomを読み込んで画面に表示するコンポーネントは次のように実装できます。
import { useRecoilValue } from "recoil"; import { helloTypographyState } from "../store/HelloTypographyState"; export function HelloTypography() { const helloTypography = useRecoilValue(helloTypographyState); return <h1>Hello, {helloTypography}</h1>; }
atomを使うことでコンポーネントが表示する文字列を、コンポーネントから切り離して管理することができました。
atom を書き込む
atomを書き込むにはuseSetRecoilState
を使います。
次のButtonChangeHelloTypography
コンポーネントでは、先ほど定義した helloTypographyState
をChangeに変更します。
import { useSetRecoilState } from "recoil"; import { helloTypographyState } from "../store/HelloTypographyState"; export function ButtonChangeHelloTypography() { const setHelloTypography = useSetRecoilState(helloTypographyState); return <button onClick={() => setHelloTypography("Change")}>Change</button>; }
これで、同じatomを共有する2つのReactコンポーネントを定義することができました。atomの値は変化が起きると値を読み込んでいるコンポーネントは最新の値で再レンダリングされる仕組みになっています。そのため、atomを書き込むコンポーネントはatomの値をどのように変更すればよいかだけに注力すればよく、再レンダリングをしなければならないReactコンポーネントや、再レンダリングのタイミングはRecoilが管理してくれます。
Scala 3 でfor内包表記を使ったコードのメモ
scala 3 を使っていて、いい感じにコードを書くことができたので、ここにメモ。
CSVから値を読み込んで直積型に変換する処理を実装していた。
登場する型はRecipeとRecipeRecord。
CSVから読み込んだデータはRecipeRecordで表現されているので、それをRecipeに変換する。
case class RecipeRecord(name: String, description: String) def convertToRecipe(recipeRecord: RecipeRecord): Either[String, Recipe] = for { _ <- validateRecipeName(recipeRecord.name) _ <- validateRecipeDescription(recipeRecord.description) recipe = Recipe(recipeRecord.name, recipeRecord.description) } yield recipe
変換をする時に、バリデーションを行う。
バリデーションで失敗すると、EitherのLeft[String]が返るようにしている。一方で、バリデーションが成功すればRight[Unit]が返る。
これをfor内包表記で使うことで、一連の処理をつなげて書くことができた。for内包表記のおかげで短絡も実現できたので、バリデーションが失敗すると関数全体の戻り値はLeft[String]になる。バリデーションが成功すればRithg[Recipe]が返される。
クライアントコードは変換に成功すればRecipe型を、失敗すれば原因を表す文字列を受け取ることができる。
JavaScriptのthisについて
関数宣言とArrow Functionのthisの違いについてのメモです。
以下のサイトに詳しく書いてあります。
関数宣言の場合、thisは動的に決まるとのこと。
ベースのオブジェクトがあればそのオブジェクトをthisとし、なければundefinedとなる、と。
ここで言うベースのオブジェクトとはブラケット演算子、ドット演算子の左側にあるオブジェクトのことです。
では、実験してみましょう。
環境はこちら。
❯ node --version v20.11.1
REPLを使っていきます。ちょっと試したいときはREPLが使いやすいと個人的には思います。
次のようにthisをコンソールに出力してみます。
関数宣言の場合、ただ呼び出すとundefinedになります。これはベースとなるオブジェクトがないからですね。
> function hoge() { ... console.log(this); ... } > hoge(); undefined
このhoge関数をhogeObjオブジェクトにセットしてみます。
そうすると、thisはhogeObjになりました。thisが実行時のコンテキストで動的に変わっているのがわかりました。
> const hogeObj = { ... hoge ... } > hogeObj.hoge(); { hoge: [Function: hoge] }
callを使えばベースとなるオブジェクトを指定しつつ呼び出すことができます。
> hoge.call({}); {}
同じようにArrow Functionでも試してみましょう。
> const fuga = () => { ... console.log(this); ... } > fuga(); <ref *1> Object [global] { global: [Circular *1], clearImmediate: [Function: clearImmediate], setImmediate: [Function: setImmediate] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, clearInterval: [Function: clearInterval], clearTimeout: [Function: clearTimeout], setInterval: [Function: setInterval], setTimeout: [Function: setTimeout] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, queueMicrotask: [Function: queueMicrotask], structuredClone: [Function: structuredClone], atob: [Getter/Setter], btoa: [Getter/Setter], performance: [Getter/Setter], fetch: [Function: fetch], crypto: [Getter], hoge: [Function: hoge] }
Arrow Functionの場合はthisを外側のスコープに探しに行きます。
node.jsの場合はglobalオブジェクトがグローバルスコープのthisのようです。
https://nodejs.org/dist/latest-v20.x/docs/api/globals.html#global-objects
そういえばdenoはどうなんだろう?
❯ deno --version deno 1.41.0 (release, x86_64-unknown-linux-gnu) v8 12.1.285.27 typescript 5.3.3
> this Window {}
Windowオブジェクトが取れました。実行環境で違いますね。
話を戻してArrow Functionのベースオブジェクトを変更してみます。
> const fugaObj = { ... fuga ... } > fugaObj.fuga(); <ref *1> Object [global] { global: [Circular *1], clearImmediate: [Function: clearImmediate], setImmediate: [Function: setImmediate] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, clearInterval: [Function: clearInterval], clearTimeout: [Function: clearTimeout], setInterval: [Function: setInterval], setTimeout: [Function: setTimeout] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, queueMicrotask: [Function: queueMicrotask], structuredClone: [Function: structuredClone], atob: [Getter/Setter], btoa: [Getter/Setter], performance: [Getter/Setter], fetch: [Function: fetch], crypto: [Getter], hoge: [Function: hoge] } > fuga.call({}); <ref *1> Object [global] { global: [Circular *1], clearImmediate: [Function: clearImmediate], setImmediate: [Function: setImmediate] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, clearInterval: [Function: clearInterval], clearTimeout: [Function: clearTimeout], setInterval: [Function: setInterval], setTimeout: [Function: setTimeout] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, queueMicrotask: [Function: queueMicrotask], structuredClone: [Function: structuredClone], atob: [Getter/Setter], btoa: [Getter/Setter], performance: [Getter/Setter], fetch: [Function: fetch], crypto: [Getter], hoge: [Function: hoge] }
オブジェクトにセットしても、callをつかっても、結果が変わらないことがわかります。
というわけで、Arrow Functionの場合はthisが静的に決定されあとから変更することができないことがわかりました。
一方で、Arrow Function自体を動的に生成する場合、外側のthisが動的に変わるのであればもちろんArrow Functionが参照するthisも生成されるごとにことなるthisを参照することになります。
例えば次のようなケースです。
> const hoge = { ... fuga() { ... return () => { console.log(this) }; ... } ... } > hoge.fuga()(); { fuga: [Function: fuga] } > hoge.fuga.call({})(); {}
hogeオブジェクトのfugaメソッドはArrow Functionを作成して返します。
Arrow Functionはthisを外側のスコープに探しに行きます。このときの外側のスコープにはfugaメソッドであり、fugaメソッドのthisはhogeオブジェクトです。
なので単純に hoge.fuga()()
と呼び出すとArrow Functionのthisはhogeオブジェクトになりました。
ただし、fugaメソッドの呼び出しをcallで行いベースオブジェクトを変更するとArrow Functionのthisも変化します。
これはArrow Functionが作成されるとき、外側のスコープを探しに行くためです。
fugaメソッドのthisが変更されたのであれば変更されたthisをArrow Functionも探しに行くことになる、ということですね。
jacksonその2:TreeModel
jacksonについて調べたときのメモその2です。
環境は前回の記事と同じですので省略します。
https://baubaubau.hatenablog.com/entry/2024/02/04/145625
TreeModelというものがどうやらあるらしいので触ってみましょう。
https://github.com/FasterXML/jackson-databind?tab=readme-ov-file#tree-model
一応wikiにもページがありましたが中身が空っぽのようです。
https://github.com/FasterXML/jackson-databind/wiki/JacksonTreeModel
TreeModel
jacksonを使うとJavaのオブジェクトとJSONまたは、ListやMapなどのコレクションなどとJSONとの間で相互変換ができますが、JSONをトラバーサルするのには向いていません。ということでTreeModelというものがあるぞ、というみたいです。
ObjectMapperをインスタンス化し、
ObjectMapper objectMapper = new ObjectMapper();
readTreeでJSON文字列を読み込みます。するとJSONのルート要素を表現するJsonNodeが返ってくるようです。
JsonNode root; root = objectMapper.readTree(""" { "NB001": { "profile": { "firstName": "book", "lastName": "store", "age": 25 }, "type": [ "software engineer" ] } } """);
トラバーサルして値を読み込むにはgetを使い目的のプロパティまで掘っていけば良さそう。
値の型に応じてasなんちゃらメソッドを呼び出せば対応するJavaの値で取得できます。
String firstName = root.get("NB001").get("profile").get("firstName").asText(); assertEquals("book", firstName); int age = root.get("NB001").get("profile").get("age").asInt(); assertEquals(25, age);
値がない場合、結果はnullになりました。
JsonNode notExist = root.get("notExist");
assertNull(notExist);
値と型が適切ではない場合も試してみました。
どうやら変換できるものは頑張ってくれますが、無理なものは無意味な値で取得されるようです。てっきり例外が発生するかと思いましたがそうではないみたい。
int firstName = root.get("NB001").get("profile").get("firstName").asInt(); assertEquals(0, firstName); // 本当は "book" String age = root.get("NB001").get("profile").get("age").asText(); assertEquals("25", age); // 数値の25が文字列に変換された
値を取得する前に対象がどんな型なのかを確認するには、isなんちゃらのメソッドを使えば良さそうです。
JsonNode firstNameNode = root.get("NB001").get("profile").get("firstName"); assertTrue(firstNameNode.isTextual()); assertFalse(firstNameNode.isInt()); JsonNode ageNode = root.get("NB001").get("profile").get("age"); assertTrue(ageNode.isInt()); assertFalse(ageNode.isTextual());
トラバーサるしつつ、JSONの一部分をオブジェクトへマッピングできるようです。
次のようなクラスを定義し、
public record Profile(String firstName, String lastName, int age) { }
treeToValueで読み込みます。
Profile profile = objectMapper.treeToValue(root.get("NB001").get("profile"), Profile.class); assertEquals(new Profile("book", "store", 25), profile);
そういえば、jacksonはネストされた構造のJSONを読み込むとどうなるのでしょうか?
こういうクラスを用意し、
record ProfileNestedJson(String firstName, String lastName, int age, ProfileNestedJsonAddress address) {
}
record ProfileNestedJsonAddress(String country, String countryCode) {
}
以下のように読み込めました。
JsonNode root = objectMapper.readTree(""" { "NB001": { "profile": { "firstName": "book", "lastName": "store", "age": 25, "address": { "country": "Japan", "countryCode": "JPN" } }, "type": [ "software engineer" ] } } """); var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJson.class); assertEquals(new ProfileNestedJson("book", "store", 25, new ProfileNestedJsonAddress("Japan", "JPN")), profileNestedJson);
Javaのクラスにはないプロパティを持つJSONはどうなるのでしょうか?
次のようにpostalCodeを追加してJSONを読み込むと例外が発生しました。てっきり無視されるかと思いましたが。
JsonNode root = objectMapper.readTree(""" { "NB001": { "profile": { "firstName": "book", "lastName": "store", "age": 25, "address": { "country": "Japan", "countryCode": "JPN", "postalCode": "000-000" } }, "type": [ "software engineer" ] } } """); var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJson.class); assertEquals(new ProfileNestedJson("book", "store", 25, new ProfileNestedJsonAddress("Japan", "JPN")), profileNestedJson);
例外は次の通り。丁寧にignorableじゃないぞと書かれています。
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "postalCode" (class org.example.TreeModelTest$TreeToValueTest$ProfileNestedJsonAddress), not marked as ignorable (2 known properties: "countryCode", "country"]) at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: org.example.TreeModelTest$TreeToValueTest$ProfileNestedJson["address"]->org.example.TreeModelTest$TreeToValueTest$ProfileNestedJsonAddress["postalCode"]) at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61) at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1153) at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:2224) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1719) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1697) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:279) at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:464) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1419) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185) at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545) at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:571) at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:440) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1419) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185) at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342) at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4875) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3033) at com.fasterxml.jackson.databind.ObjectMapper.treeToValue(ObjectMapper.java:3497) at org.example.TreeModelTest$TreeToValueTest.treeToValueNestedJsonAdditionalProperties(TreeModelTest.java:157) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
どうやら @JsonIgnoreProperties
で無視するプロパティを指定する必要があるとのこと。
https://github.com/FasterXML/jackson-databind?tab=readme-ov-file#annotations-ignoring-properties
というわけで次のようにしたら動きました。
record ProfileNestedJsonAdditionalProperties(String firstName, String lastName, int age, ProfileNestedJsonAdditionalPropertiesAddress address) { } @JsonIgnoreProperties("postalCode") record ProfileNestedJsonAdditionalPropertiesAddress(String country, String countryCode) { } JsonNode root = objectMapper.readTree(""" { "NB001": { "profile": { "firstName": "book", "lastName": "store", "age": 25, "address": { "country": "Japan", "countryCode": "JPN", "postalCode": "000-000" } }, "type": [ "software engineer" ] } } """); var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJsonAdditionalProperties.class); assertEquals(new ProfileNestedJsonAdditionalProperties("book", "store", 25, new ProfileNestedJsonAdditionalPropertiesAddress("Japan", "JPN")), profileNestedJson);
jacksonその1
jacksonについて調べてみたときのメモです。
jacksonのリポジトリはこちら。
https://github.com/FasterXML/jackson
jacksonとは、JavaのJSONライブラリ、というところまでは前提知識があります。
以前からそれとなく使っていて、Spring frameworkを使っていればフレームワークがデフォルトで使用しているはずです。
jacksonは3つの要素から構成されているようです。
- https://github.com/FasterXML/jackson-core
- https://github.com/FasterXML/jackson-databind
- https://github.com/FasterXML/jackson-annotations
他にもコミュニティによるモジュールがあるようですが、上記のモジュールはjackson開発チームで開発、メンテナンスがされているそう。
サードパーティのモジュール一覧はこちらに記載がありました。思っていたより多いです。
https://github.com/FasterXML/jackson?tab=readme-ov-file#third-party-datatype-modules
databindリポジトリにチュートリアルがありました。やってみましょう。
https://github.com/FasterXML/jackson-databind
環境
java -version openjdk version "17.0.8.1" 2023-08-24 OpenJDK Runtime Environment Temurin-17.0.8.1+1 (build 17.0.8.1+1) OpenJDK 64-Bit Server VM Temurin-17.0.8.1+1 (build 17.0.8.1+1, mixed mode, sharing)
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>jackson-my-playground</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <jackson.version>2.16.0</jackson.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.junit</groupId> <artifactId>junit-bom</artifactId> <version>5.10.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
read and write
package org.example; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class ReadWriteTest { @Test void readValue() throws JsonProcessingException { var objectMapper = new ObjectMapper(); var person = objectMapper.readValue(""" { "firstName": "book", "lastName": "store" } """, Person.class); assertEquals("firstName: book, lastName: store", person.toString()); } @Test void writeValue() throws JsonProcessingException { var objectMapper = new ObjectMapper(); var output = objectMapper.writeValueAsString(new Person("book", "store")); assertEquals("{\"firstName\":\"book\",\"lastName\":\"store\"}", output); } public static class Person { @SuppressWarnings("unused") // jackson use public Person() { } public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String firstName; public String lastName; @Override public String toString() { return String.format("firstName: %s, lastName: %s", firstName, lastName); } } }
jacksonによるJSONの読み込み、書き込みはObjectMapper
を使います。ObjectMapper
クラスはインスタンス化することで使用できます。
JSONをPOJOで読み込むにはreadValue
を使います。反対に書き込むにはwriteValue
を使います。
書き込んだ結果をString
で貰いたいのであれば、writeValueAsString
を使います。
読み込む先のPOJOはデフォルトコンストラクタ(引数なしコンストラクタ)が必要になりました。おそらくですが、読み込み元のJSONをPOJOにどうやって変換するかどうかは変更できると思うので、上記のコードのようにデフォルトでは必要ということでしょう。
collection
JSONの読み込み、書き込みはPOJOだけではありません。以下のようにCollectionのMapやListを指定することも可能です。
package org.example; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import org.junit.jupiter.api.Test; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class GenericCollectionTest { final ObjectMapper objectMapper = new ObjectMapper(); @Test void readAsMap() throws JsonProcessingException { @SuppressWarnings("unchecked") Map<String, String> result = objectMapper.readValue(""" { "firstName": "book", "lastName": "store" } """, Map.class); assertEquals(Map.of("firstName", "book", "lastName", "store"), result); } @Test void readAsList() throws JsonProcessingException { @SuppressWarnings("unchecked") List<Integer> result = objectMapper.readValue(""" [1, 2, 3] """, List.class); assertEquals(List.of(1, 2, 3), result); } @Test void cantReadObjectToList() { assertThrows(MismatchedInputException.class, () -> objectMapper.readValue(""" { "firstName": "book", "lastName": "store" } """, List.class)); } @Test void readAsMapPersonRecord() throws JsonProcessingException { var people = objectMapper.readValue(""" { "NB001": { "firstName": "book", "lastName": "store" } } """, new TypeReference<Map<Id, Person>>() { }); assertEquals(Map.of(new Id("NB001"), new Person("book", "store")), people); } public record Id(String value) { } public record Person(String firstName, String lastName) { } }
readAsMapPersonRecord
ではMapで読み込みをしつつ、valueにRecordクラスを指定しました。
このように、JSONをどのように変換するのか一つの型に収まらない場合はTypeReference
を使えば良さそうです。
短絡評価:JavaScriptにおける値の評価順序と動作
- 値が定まった時点で評価をそれ以上行わないことを短絡評価と呼ぶ。
- 例えば[JavaScript]では
&&
演算子が該当する。 &&
は左から順に評価しtrueだった場合は右辺の評価を続ける。- 右辺が最後の評価対象の場合はその評価結果を返す。
- true以外だった場合(false, undefined, 2)評価をそれ以上行わず、その時点の評価結果を返す。
> true && console.log("hello"); hello undefined > false && console.log("hello"); false > console.log("hello") && "msg"; hello undefined
&&
は暗黙的な型変換を行い、値を真偽値に変換する。false
に変換される値はfalsyな値と呼ばれる。- falsyな値のリスト以外の値はすべてtrueに変換される。
複数つなげた場合の動作
&&
を複数つなげて書くと短絡評価は左から右へと順番に処理される。- すべての値がtrueの場合の動作。
- 1番目の値を評価し、trueなので真ん中の値を評価する。
- 2番目の値を評価し、trueなので最後の値を評価する。
- 3番目の値を評価し、結果として返す。
> true && true && 'hello 1' 'hello 1' > 'hello 1' && 'hello 2' && 'hello 3' 'hello 3'
- 途中の値がfalseの場合の動作。
- 中央の値を評価し、falsyな値のためその値を結果として返す。
> true && null && 'hello' null
- 最後の値がfalseの場合の動作。
- 最後の値を評価し、falsyな値のためその値を結果として返す。
> true && true && null null