【Flutter】 Either型を使ってみよう!【fpdart】

Either 型? どんな型なんだろう?

エラーハンドリングをもっと楽にする方法ってないかしら?

本記事ではそんな疑問にお答えします。

Flutter のエラーハンドリングを楽にしてくれる、Either型について解説します。

Either型について紹介した上で、
try - catch でエラーハンドリングを行った場合のコードを改良します。

Eitherを使って記述すると、以下の画像のように短いコードで端的に表すことができます。

ぜひ読んでみてください!

Either 型とは

解決したい課題

入力された文字列を数字に変換し、その数字の平方根を返すアプリケーションを考えます。

ユーザーが素直に数字を入力してくれればよいですが、
入力フォームに文字列を入力した際に、エラー(例外)が発生します。

ここで、エラーハンドリングが必要となります。
エラーハンドリングの一例がtry-catchを使う方法です。

try {
  number = double.parse(text);
} on FormatException catch (e) {
  //エラー時の対応
}

try{}内の処理を実行し、
例外が発生したらon FormatException catch (e) {}内の処理が実行される、という方法です。

上のコードではdouble.parse()のメソッドは()内に
文字列が入力された場合にFormatExceptionという例外が発生するので、
それをcatchで対処できるようになっています。

この方法自体に問題はないのですが、
try-catchを埋め込んだメソッドを作成する際に、少し困ることが起き得ます。

それは、正常時と例外時で違う型の結果を返したい時です。

今回のparseのように、正常時にはparseした結果の数値を返したい、
例外時には例外を返したい、といった場合、
メソッドの返り値を何で設定していいか困ります。

最終的にUIの表示をStringにしようとするのであれば、
メソッド内でStringへの変換処理を記載することで返す型をStringに統一できますが、
メソッドが肥大化します。
また、メソッドで行うことが、
「メソッドで本来やりたいこと」+「Stringへの変換」
となり、実行内容が分散し読みにくくなってしまいます。

メソッド埋め込みをやめて、UIの記述文(build メソッド内)でBuilder等を使い、
try-catchを直接行うのも、buildメソッドが肥大化し、読みにくくなってしまいます。

try-catchをメソッドに埋め込みつつ、正常時と例外時で別々の型を返したい」
というのが今回解決したい課題となります。

Either型での解決

上記課題を解決してくれるのが、fpdartパッケージEither型です。

Either型とは抽象クラスで、
正常時用にEither型を継承したRight型が、
例外時用に同じくEither型を継承したLeft型がそれぞれ用意されています。

Right型、Left型、共に内部に任意の型の値を保持することができます。

上記の、
try-catchをメソッドに埋め込みつつ、正常時と例外時で別々の型を返したい」
という課題については、
Either型を返り値として設定し、
正常時にはRight型を、例外時にはLeft型を返す、という方法で解決できます。

  Either<FormatException, double> parseNumber(String text) {
    try {
      return Either.right(double.parse(text));
    } on FormatException catch (e) {
      return Either.left(e);
    }
  }

上記例では、正常時にはdouble型を保持するRight型の値を、
例外時にはFormatException型を保持するLeft型の値を返します。

まとめるとEither型とは、返り値の型が2つの型どちらにもなりうる際に対応できる型です。

この後の基本的な使い方ではエラーハンドリングにより特化した使い方と、
Either型からの値の取り出し方について解説します。

基本的な使い方

以下のコードを修正していく形で解説していきます。
(先に紹介した比較画像の左側のコードです。)

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyWidget(),
    );
  }
}

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late final TextEditingController _controller;

  @override
  void initState() {
    _controller = TextEditingController();
    _controller.text = '0';
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            SizedBox(
              width: 100,
              child: TextField(
                controller: _controller,
                onChanged: (value) => setState(() {}),
              ),
            ),
            Builder(
              builder: (context) {
                final double number;
                try {
                  number = double.parse(_controller.text);
                } on FormatException catch (e) {
                  return Text(
                    e.toString(),
                    style: const TextStyle(fontSize: 24),
                  );
                }
                return Text(
                  'sqrt($number) =${sqrt(number).toString()}',
                  style: const TextStyle(fontSize: 24),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

準備

まず準備として、パッケージのインストールと、
Dartファイルへのインポート文の追加を行います。

パッケージのインストール

CLI(macならターミナル)で、自分のプロジェクトのルートにて
以下のコマンドを実行しパッケージをインストールします。

flutter pub add fpdart

パッケージのインポート

実装したいDartファイルの上部に以下のインポート文を追加し、
パッケージをインポートします。

一部クラスがmaterialパッケージと競合するため、as文でfpという名前付けをしておきます。

import 'package:fpdart/fpdart.dart' as fp;

実装

Either型のメソッドを用いたtry-catch

Either型とは」の章では以下のコードを紹介しました。

  Either<FormatException, double> parseNumber(String text) {
    try {
      return Either.right(double.parse(text));
    } on FormatException catch (e) {
      return Either.left(e);
    }
  }

このコードをEithertryCatchメソッドで書き換えることができます。

  Either<FormatException, double> parseNumber2(String text) {
    return Either.tryCatch(
      () => double.parse(text),
      (e, s) => e as FormatException,
    );
  }

第1引数にてtry{}文の中に書く処理を記述し、正常時に返したい値を返します。
第2引数にて例外時の処理を記述し、例外時に返したい値を返します。
eは例外時のObject, sStackTraceです。)

このメソッドをMyWidget内、buildメソッドの後に追加します。
(Eitherfp.Either)となることに注意です。

  fp.Either<FormatException, double> parseNumber2(String text) {
    return fp.Either.tryCatch(
      () => double.parse(text),
      (e, s) => e as FormatException,
    );
  }

Either型からの値の引き出し

Either型からの値の引き出し方を解説します。

値を引き出す際にはmatchメソッドを用いるのが便利です。

matchメソッドは、引数にてEither型の値がRight型だった際の関数と、
Left型だった際の関数を記述することで、共通の型を返すことのできるメソッドです。

Either.match<String>(
  (l) => l.toString(),
  (r) => 'sqrt($r) = ${sqrt(r).toString()}',
),

l,rはそれぞれLeft型, Right型の中身の値を表します。
引数にてStringを返すメソッドを設定しています。
matchメソッドを用いることで、それぞれの型ごとに別々の処理を行いつつ、
一つの型に変換することができます。

ベースコードのBuilderウィジェットを以下のウィジェットに置き換えてください。

            Text(
              parseNumber2(_controller.text).match<String>(
                (l) => l.toString(),
                (r) => 'sqrt($r) = ${sqrt(r).toString()}',
              ),
              style: const TextStyle(fontSize: 24),
            ),

以上で書き換えは完了となります。

最終的なコードは以下のとおりです。

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:fpdart/fpdart.dart' as fp;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyWidget(),
    );
  }
}

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late final TextEditingController _controller;

  @override
  void initState() {
    _controller = TextEditingController();
    _controller.text = '0';
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            SizedBox(
              width: 100,
              child: TextField(
                controller: _controller,
                onChanged: (value) => setState(() {}),
              ),
            ),
            Text(
              parseNumber2(_controller.text).match<String>(
                (l) => l.toString(),
                (r) => 'sqrt($r) = ${sqrt(r).toString()}',
              ),
              style: const TextStyle(fontSize: 24),
            ),
          ],
        ),
      ),
    );
  }

  fp.Either<FormatException, double> parseNumber2(String text) {
    return fp.Either.tryCatch(
      () => double.parse(text),
      (e, s) => e as FormatException,
    );
  }
}

まとめ

本記事ではFlutter のエラーハンドリングを楽にしてくれる、Either型について解説しました。

Either型について紹介した上で、
try - catch でエラーハンドリングを行った場合のコードを改良します。

いかがだったでしょうか?

Either型を用いることで、コードがスッキリと、見やすくなりました。

Either型には他にもさまざまなメソッドが用意されています。
こちらにて紹介されていますので、興味ある方はぜひ見てみてください。

本記事があなたのアプリ開発の一助となれば幸いです。

Flutterを一緒に学んでみませんか?
Flutter エンジニアに特化した学習コミュニティ、Flutter大学への入会は、
以下の画像リンクから。



参考

Functional Error Handling with Either and fpdart in Flutter: An Introduction
Fpdart aims to bring all the main types found in functional languages to Dart. Here we focus on the Either type and learn how to use it for robust error handlin...

編集後記(2022/8/18)

Either型についての記事でした。

今回は深く触れませんでしたが、
このEither型が含まれるパッケージ、
fpdartは関数型プログラミングを補助するためのパッケージです。

関数型プログラミングとはデータと関数を分離して記述していくプログラミング手法です。
関数の組み合わせで記述していくことにより、
コードがシンプルに、読みやすくなる、というメリットがあります。

(詳しくは、こちらの記事を読んでみることをオススメします。)

意識しなければ気が付かないような内容ではありますが、
コードをよりシンプルにわかりやすくしたい、
といった時には使用を検討してみてもよいかと思います。

自分もまだまだ勉強中ですので、興味のある方は一緒に勉強しましょう!

週刊Flutter大学では、Flutterに関する技術記事、Flutter大学についての紹介記事を投稿していきます。
記事の更新情報はFlutter大学Twitterにて告知します。
ぜひぜひフォローをお願いいたします。

タイトルとURLをコピーしました