yucatio@システムエンジニア

趣味で作ったものいろいろ

FirestoreでEnum型風ルールを作成する

Firestoreではセキュリティルールでフィールドの型を指定できます。 フィールドの型にはboolintstringなどがあります。 しかしenum型は存在しません。

Firebaseのセキュリティルールでは、string型のフィールドに対して登録できる文字列を制限することができます。 この機能を使うことでenum型風のフィールドを作成することができます。

String.matches(regex)

String.matchesを使うと、フィールドに登録される値が 指定された正規表現を満たす場合にのみ、DBに登録することができます。 記述はこのようになります。

allow create, update: if request.resource.data.status.matches("^(TODO|IN_PROGRESS|DONE)$")

String.matches(regex)の使用例

例としてTODOアプリを考えます。入力はタスク名とステータス(実施状況)です。

f:id:yucatio:20211017171414p:plain:w300

データ構造はこのようになっています。

f:id:yucatio:20211017230600p:plain:w350

status(ステータス)には"TODO"(未着手)か"IN_PROGRESS"(作業中)か"DONE"(完了)のみ指定できます。

これをセキュリティルールで書くとこのようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todo} {
      function validTodo(docData) {
        return docData.status.matches("^(TODO|IN_PROGRESS|DONE)$")
      }
      
      allow read: if true;
      allow create, update: if validTodo(request.resource.data);
    }
  }
}

このセキュリティルールでステータスが"TODO"のタスクを登録します。 Firebaseのコンソールで確認すると登録されていることが分かります。

f:id:yucatio:20211017172045p:plain

もし"IN_PROGRESS"を"DOING"と間違えてしまった場合、"Missing or insufficient permissions."のエラーが出ます。DBを確認すると登録ができていないことが確認できます。

f:id:yucatio:20211017172609p:plain
google chromeのコンソール

最後に、念のため正規表現があっているか確かめておきましょう。

https://jex.im/regulex/#!flags=&re=%5E(TODO%7CIN_PROGRESS%7CDONE)%24

f:id:yucatio:20211017171755p:plain

大丈夫そうですね。

MUI(Material-UI)のアイコンにフチドリをつける

MUI(Material-UI)のアイコンにフチドリをつけます。

f:id:yucatio:20211017140609p:plain

通常のCSSのスタイルと同様にstrokestrokeWidthを指定します。strokeOpacitystrokeLinejoinなども指定できます。

コード

<StarIcon
  sx={{color: yellow[500], stroke: yellow[900], strokeWidth: 4, strokeOpacity: 0.2}}
/>

ソースコード

import React from 'react';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import StarIcon from '@mui/icons-material/Star';
import FavoriteIcon from '@mui/icons-material/Favorite';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import AppleIcon from '@mui/icons-material/Apple';
import BedtimeIcon from '@mui/icons-material/Bedtime';
import CloudIcon from '@mui/icons-material/Cloud';
import { blue, grey, red, pink, yellow } from '@mui/material/colors';

function App() {
  return (
    <Box sx={{p:2}}>
      <Typography variant="h6">通常</Typography>
      <Stack spacing={2} direction="row">
        <StarIcon sx={{color: yellow[500]}} />
        <FavoriteIcon sx={{color: pink[500]}} />
        <AddCircleIcon sx={{color: blue[500]}} />
        <AppleIcon sx={{color: red[500]}} />
        <BedtimeIcon sx={{color: yellow[600]}} />
        <CloudIcon sx={{color: grey[500]}} />
      </Stack>

      <Typography variant="h6">フチドリあり</Typography>
      <Stack spacing={2} direction="row">
        <StarIcon sx={{color: yellow[500], stroke: yellow[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <FavoriteIcon sx={{color: pink[500], stroke: pink[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <AddCircleIcon sx={{color: blue[500], stroke: blue[800], strokeWidth:4, strokeOpacity: 0.2}} />
        <AppleIcon sx={{color: red[500], stroke:red[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <BedtimeIcon sx={{color: yellow[600], stroke: yellow[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <CloudIcon sx={{color: grey[500], stroke:grey[800], strokeWidth: 4, strokeOpacity: 0.2}} />
      </Stack>
    </Box>
  );
}
export default App;

環境

  • Material-UI: 5.0.4
  • React: 11.4.1

PythonでSetのSetを作成する

PythonでSetのSetを作成しようとするとエラーになります。

my_set = set()
my_set.add({1, 2, 3})
# => TypeError: unhashable type: 'set'

Pythonでは、Setの各要素はハッシュ可能(hashable)でなければいけません。 Pythonの組み込み型でハッシュ可能なのは、int, str, tuple, frozensetです。list. dict, setはunhashableハッシュ不可能(unhashable)です。

ハッシュ可能についてはこちらの記事を参考にしました。

qiita.com

frozensetを使う

pythonでsetのsetを作成するには、frozensetを使用します。frozensetはイミュータッブルでハッシュ可能な型です。

my_set = set()

my_set.add(frozenset({1, 2, 3}))
my_set.add(frozenset({4, 5}))

print(my_set)
#=> {frozenset({1, 2, 3}), frozenset({4, 5})}

エラーなく動作しました。

もう少し挙動を確認してみます。

my_set = set()

# setにsetを追加する
my_set.add(frozenset({1, 2, 3}))
my_set.add(frozenset({1, 2}))
my_set.add(frozenset({2, 1}))  # 集合としては1つ上と同じ
my_set.add(frozenset({5, 4, 3}))
my_set.add(frozenset({4, 3, 5, 3, 4}))  # 集合としては1つ上と同じ
my_set.add(frozenset({4}))
my_set.add(frozenset({10, 2, 4, 7, 2}))
# addの回数は7回

print(len(my_set))
# => 5

print(my_set)
# => {frozenset({1, 2}), frozenset({10, 2, 4, 7}), frozenset({3, 4, 5}), frozenset({1, 2, 3}), frozenset({4})}
# {1, 2} と {2, 1} は同一のものなので、setには片方が登録されている
# {5, 4, 3} と {5, 4, 3, 3, 4} は同一のものなので、setには片方が登録されている

print({1, 2} in my_set)
# => True
# setとの比較は可能

print({3, 5, 4} in my_set)
# => True
# {3, 4, 5}と同一のものとして判定される

print({2} in my_set)
# => False
# 2が含まれる要素もあるが、my_setの要素としては一致しないのでFalseとなる

以上で、setのsetの作成と、動作確認ができました。

再帰を使わない順列生成 : 1から9の数字を1回ずつ使ってできる数を、小さい方から順に列挙する(アルゴリズム編)

1から9の数字を1回ずつ使ってできる数を、小さい方から順に列挙するアルゴリズムです。

次に小さい数を求めるアルゴリズム

ある数Nが与えられたとき、その数に使用されている数字を使用してできる数で、Nの次に大きい数を生成します。

N10^iの桁を、n[i]で表します

n[i]を桁小さい方からみていき、初めてn[i] < n[i-1]となるiを求めます

n[0]からn[i-1]まで順番に見ていき、n[i]より初めて大きくなるものを求めます。求めたものをn[j]とします。

n[i]n[j]を入れ替えます

n[0]からn[i-1]を逆順にします

この説明だとよくわからないと思うので、具体的な数字でアルゴリズムを確かめてみましょう。

アルゴリズムを適用した例

N=179836542 とします。

N10^iの桁を、n[i]で表します

以下のようになります。

n[0] = 2  // 一の位 (10^0)
n[1] = 4  // 十の位 (10^1)
n[2] = 5  // 百の位 (10^2)
n[3] = 6  // 千の位 (10^3)
n[4] = 3  // 万の位 (10^4)
n[5] = 8  // 十万の位 (10^5)
n[6] = 9  // 百万の位 (10^6)
n[7] = 7  // 千万の位 (10^7)
n[8] = 1  // 億の位 (10^8)

f:id:yucatio:20200326160139p:plain

n[i]を桁小さい方からみていき、初めてn[i] < n[i-1]となるiを求めます

小さい桁から見て行ったとき、3 < 6のとき初めて数が小さくなります。このときn[4] < n[3]なので、i = 4です。

f:id:yucatio:20200326160231p:plain

n[0]からn[i-1]まで順番に見ていき、n[i]より初めて大きくなるものを求めます。求めたものをn[j]とします。

10^4より下の桁で3より大きい数が初めて出てくるのは4です。n[1] = 4なので、j = 1です。

f:id:yucatio:20200326160423p:plain

n[i]n[j]を入れ替えます

34を入れ替えます。入れ替えた後も、n[0]からn[i-1]までは昇順に並んでいますね。

f:id:yucatio:20200326160434p:plain

n[0]からn[i-1]を逆順にします

入れ替えた結果です。

f:id:yucatio:20200326160448p:plain

以上で、179836542から、次に大きい数である、179842356を得ることができました。

小さい方から列挙するアルゴリズム

1から9の数字を1回ずつ使ってできる数の中で、一番小さい数は123456789です。 ここから、上記アルゴリズムを使用して、次に小さい数を求めます。 さらにそこから得られた数に対して上記アルゴリズムを適用します。

これを繰り返すことによって順列を取り出すことができます。

手順の、

n[i]を桁小さい方からみていき、初めてn[i] < n[i-1]となるiを求めます

ここで該当するiがなくなったら終了です。

数字が重複しているとき

数が重複しているとき、も上記アルゴリズムはうまく動作します。

例えばN=652331の場合を考えます。

N10^iの桁を、n[i]で表します

f:id:yucatio:20200330085020p:plain

n[i]を桁小さい方からみていき、初めてn[i] < n[i-1]となるiを求めます こちらは上記と同じで、

n[0] = 1  // 一の位 (10^0)
n[1] = 3  // 百の位 (10^2)
n[2] = 3  // 千の位 (10^3)
n[3] = 2  // 万の位 (10^4)
n[4] = 5  // 十万の位 (10^5)
n[5] = 6  // 百万の位 (10^6)

2 < 3のとき初めて数が小さくなります。このときn[3] < n[2]なので、i = 3です。

f:id:yucatio:20200330085047p:plain

n[0]からn[i-1]まで順番に見ていき、n[i]より初めて大きくなるものを求めます。求めたものをn[j]とします。

2より下の桁で2より大きい数で、その中で最小なのは3です。3は2つありますが、桁が小さい方を選択します。桁が小さい方は n[1] = 3なので、j = 1です。

f:id:yucatio:20200330085113p:plain

n[i]n[j]を入れ替えます 32を入れ替えます。

f:id:yucatio:20200330085150p:plain

n[0]からn[i-1]を逆順にします

f:id:yucatio:20200330085217p:plain

以上で、652331から、次に大きい数である、653123を得ることができました。

応用 : n個の数のうちk個を使用する組み合わせ

上記のアルゴリズムは、n個の数を全て使用する場合の順列を列挙しました。 n個の数のち、k個を使用する場合のアルゴリズムは以下をご覧ください。

★記事作成中

Groovyのクロージャ ④Groovyで独自のDSLの作成

背景

JenkinsというCIツールではGroovyのコードでCI挙動を記述できるJenkins pipelineがあります。 ここではGroovyのDSL(Domain-Specific Language:ドメイン固有言語)が使われています。 DSLにはクロージャが効果的に使用されています。

今回は独自のDSLの作成です。

前回の記事

前回の記事で、 クロージャの名前解決の方法の各挙動を確認しました。

yucatio.hatenablog.com

今回は、クロージャと名前解決方法を利用して、独自のDSLを定義します。

DSLとは

DSLとは特定のタスク向けに設計されたコンピュータ言語である。

ドメイン固有言語 - Wikipedia

例えば、文章の構造を表すDSL(XMLなど)やページの装飾に特化したDSL(CSSなど)、データの取得に特化したDSL(SQLなど)が挙げられます。

特定のタスクに向けて設計されるDSLは、プログラミング経験者以外が使用することを前提としたものもあります。また、人が自然に読めるように配慮されているものも多くあります。

記述には、宣言型のスタイルがよく用いられます。 宣言型のスタイル(宣言型プログラミング)とは、

対象の性質を宣言してプログラムを構成するプログラミングパラダイム、あるいはそのような性質をもったプログラミングパラダイムの総称である。

宣言型プログラミング - Wikipedia

宣言型プログラミングでは、どのように処理をするかは書かれておらず、何を処理するか(対象)を記述(宣言)します。

例: SQLの場合

SELECT id, name, price FROM item WHERE price >= 100
-- itemテーブルからpriceが100以上の行を見つけ、その行のidとnameとpriceを取得するという意味のSQL
-- 具体的にどのように取ってくるかは書かれていない

GroovyでのDSLの実現方法

Groovyでは以下のようなDSLを作成できます。何を表すか(するか)という短い説明の後に、内容を記載します。 子要素がある場合は{}で囲います。

report {
    title "DSLの説明"
    date "2020-02-03"
    body {
        section("クロージャとは") {
            paragraph  "クロージャとは生まれ故郷(クロージャ自身が定義された場所)を忘れない無名関数のことです。"
        }
        section("thisとownerとdelegate") {
            paragraph  "thisはクロージャを囲んでいるクラスです。"
            paragraph  "ownerはクロージャを囲んでいるオブジェクトです。クラスか、クロージャを囲んでいるクロージャです。"
            paragraph  "delegateは外部のオブジェクトです。メソッドやプロパティ名が元のオブジェクトで解決できないときに呼ばれます。"
        }
    }
}

この記法はプログラミング言語っぽくないですが、groovyの言語仕様に従って記載されています。{}で囲まれた部分はクロージャです。reporttitlesectionなどはメソッドです。

今回は以下のような簡単なDSLを作成します。

  • reportディレクティブはレポートを作成するためのトップ階層です
  • reportの下にはtitle、date、bodyのみ記載することができます
  • title、dateには文字列を続けて書きます
  • bodyの子要素はsectionのみです
  • sectionの子要素はparagraphのみです
  • paragraphには文字列を続けて書きます

reportメソッドの作成

reportメソッドは以下のようになります。クロージャdelegateReportSpecクラスのインスタンスを指定し、resolveStrategyClosure.DELEGATE_ONLYに設定します。

def report(Closure cl) {
    cl.delegate = new ReportSpec()
    cl.resolveStrategy = Closure.DELEGATE_ONLY
    cl()
}

移譲先のクラスの作成

ReportSpecクラスです。titleメソッドとdateメソッドは対応する文字列を表示します。bodyメソッドは、クロージャを引数にとり、BodySpecクラスのインスタンスdelegateに設定しています。

class ReportSpec {
    void title(String title) { println "<h1>${title}</h1>"}
    void date(String date) { println "<div>作成日 : ${date}</div>"}
    void body(Closure body) {
        body.delegate = new BodySpec()
        body.resolveStrategy = Closure.DELEGATE_ONLY
        body()
    }
}

同様に、BodySpecは、以下です。

class BodySpec {
    void section(String title, Closure section) {
        println "<h2>${title}</h2>"
        section.delegate = new SectionSpec()
        section.resolveStrategy = Closure.DELEGATE_ONLY
        section()
    }
}

同様に、SectionSpecは、以下です。

class SectionSpec {
    void paragraph(String paragraph) {println "<p>${paragraph}</p>"}
}

実行結果

以下のDSLを実行します。

report {
    title "DSLの説明"
    date "2020-02-03"
    body {
        section("クロージャとは") {
            paragraph  "クロージャとは生まれ故郷(クロージャ自身が定義された場所)を忘れない無名関数のことです。"
        }
        section("thisとownerとdelegate") {
            paragraph  "thisはクロージャを囲んでいるクラスです。"
            paragraph  "ownerはクロージャを囲んでいるオブジェクトです。クラスか、クロージャを囲んでいるクロージャです。"
            paragraph  "delegateは外部のオブジェクトです。メソッドやプロパティ名が元のオブジェクトで解決できないときに呼ばれます。"
        }
    }
}

実行結果です。

<h1>DSLの説明</h1>
<div>作成日 : 2020-02-03</div>
<h2>クロージャとは</h2>
<p>クロージャとは生まれ故郷(クロージャ自身が定義された場所)を忘れない無名関数のことです。</p>
<h2>thisとownerとdelegate</h2>
<p>thisはクロージャを囲んでいるクラスです。</p>
<p>ownerはクロージャを囲んでいるオブジェクトです。クラスか、クロージャを囲んでいるクロージャです。</p>
<p>delegateは外部のオブジェクトです。メソッドやプロパティ名が元のオブジェクトで解決できないときに呼ばれます。</p>

レポートが作成されました。

rehydrateメソッドの使用

今回、report, reportSpec, BodySpec内で、クロージャdelegateを以下のように指定しました。

def report(Closure cl) {
      cl.delegate = new ReportSpec()
      cl.resolveStrategy = Closure.DELEGATE_ONLY
      cl()
}

一般的に、引数として受け取った変数の内容は変更すべきではありません。 今回は引数として受け取ったクロージャdelegateフィールドを書き換えていたので、この制約を破っています。

この状態を避けるために、クロージャをコピーし、コピーしたものに対してdelegateを変更します。 幸いなことに、コピーとdelegateの変更を一度に行うrehydrateというメソッドが用意されています。 (hydrateは水分を補給するという意味) rehydrateではクロージャをコピーして、ownerthisdelegateを引数で与えられたオブジェクトに変更したものを返します。

reportメソッドをrehydrateで書き換えます。ownerthisObjの部分にthisを指定していますが、下で名前解決方法をClosure.DELEGATE_ONLYに指定しているので、ownerthisの部分は何でも構いません。

def report(Closure cl) {
  def reportSpec = new EmailSpec()
  def clCopy = cl.rehydrate(reportSpec, this, this)
  clCopy.resolveStrategy = Closure.DELEGATE_ONLY
  clCopy()
}

以上で独自のDSLの作成できました。

参考リンク

The Apache Groovy programming language - Domain-Specific Languages

Groovyのクロージャ ③名前解決方法(resolveStrategy)の挙動

背景

JenkinsというCIツールではGroovyのコードでCI挙動を記述できるJenkins pipelineがあります。 Jenkins pipelineではGroovyのDSL(Domain-Specific Language:ドメイン固有言語)が使われています。 DSLにはクロージャが効果的に使用されています。

今回はクロージャの名前解決方法(resolveStrategy)です。

前回の記事

前回の記事で、 クロージャは故郷を忘れないためにthisownerに生まれた場所の情報を格納していることが確認できました。また、クロージャで呼んでいるメソッドがownerで定義されていない場合にdelegateのメソッドを呼ぶことが分かりました。

yucatio.hatenablog.com

名前解決とは

名前解決とは、ここでは、「メソッドやプロパティ名が現れたとき、どこに定義されている名前を使用するか決めること」とします。

クロージャの名前解決 (resolveStrategy)

クロージャは、内部で呼んでいるメソッドがownerで定義されていない場合にdelegateのメソッドを呼びます。この挙動はクロージャオブジェクトのresolveStrategyプロパティに値をセットすることで変更できます。セットできるのは以下の5つです。

名前解決方法 説明
Closure.OWNER_FIRST デフォルト。プロパティ/メソッドがownerに存在するときは、ownerのものを使用し、なければdelegateのものを使用する
Closure.DELEGATE_FIRST OWNER_FIRSTの逆。プロパティ/メソッドがdelegateに存在するときは、delegateのものを使用し、なければownerのものを使用する
Closure.OWNER_ONLY プロパティ/メソッドの名前解決をownerのみで行う。delegateは無視される
Closure.DELEGATE_ONLY プロパティ/メソッドの名前解決をdelegateのみで行う。ownerは無視される
Closure.TO_SELF 名前解決はクロージャクラスで行われる。開発者がクロージャのサブクラスを作成して、クロージャの振る舞いをカスタマイズしたい場合にこの設定にする

名前解決方法の挙動の確認

OWNER_FIRSTDELEGATE_FIRSTOWNER_ONLYDELEGATE_ONLYの動作を確認するため、MainクラスとPipelineSpecクラスそれぞれに以下のようにメソッドを定義します。

a() b() c()
Main ×
PileineSpec ×
class PipelineSpec {
    def b() {
        println "I'm PipelineSpec.b"
    }
    def c() {
        println "I'm PipelineSpec.c"
    }
}

class Pipeline {
    def pipeline(Closure cl) {
      cl.delegate = new PipelineSpec()
      // ここを書き換えててテストする
      cl.resolveStrategy = Closure.OWNER_FIRST
      println "before closure"
      cl()
      println "after closure"
    }
}

class Main {
    // 普通のインスタンスメソッド
    def closureTest() {
        def pipeline = new Pipeline()
        pipeline.pipeline {
            a()
            b()
            c()
        }
    }
    
    def a() {
        println "I'm Main.a"
    }
    def b() {
        println "I'm Main.b"
    }
}

def main = new Main()
main.closureTest()

OWNER_FIRST

上記のコードを実行した結果です。

before closure
I'm Main.a
I'm Main.b
I'm PipelineSpec.c
after closure

a()メソッドとb()メソッドはMainクラスに定義したものが呼ばれ、c()メソッドはPipelineSpecのものが呼ばれました。

DELEGATE_FIRST

上記コードを下記のように書き換えて実行します。

      // ここを書き換えててテストする
      cl.resolveStrategy = Closure.DELEGATE_FIRST

実行結果です。

before closure
I'm Main.a
I'm PipelineSpec.b
I'm PipelineSpec.c
after closure

a()メソッドはMainクラスに定義したものが呼ばれ、b()メソッドとc()メソッドはPipelineSpecのものが呼ばれました。

OWNER_ONLY

上記コードを下記のように書き換えて実行します。

      // ここを書き換えててテストする
      cl.resolveStrategy = Closure.OWNER_ONLY

実行結果です。

before closure
I'm Main.a
I'm Main.b
Exception thrown

groovy.lang.MissingMethodException: No signature of method: Main.c() is applicable for argument types: () values: []
Possible solutions: a(), b(), is(java.lang.Object), any(), tap(groovy.lang.Closure), any(groovy.lang.Closure)
    at Main$_closureTest_closure1.doCall(closure_01.groovy:27)
    at Main$_closureTest_closure1.doCall(closure_01.groovy)
    // 以下略

Mainクラスに定義したa()メソッドとb()メソッドが呼ばれたあと、名前解決が失敗し、MissingMethodExceptionが発生しました。 PipelineSpecでの名前解決が行われなかったことがわかります。

DELEGATE_ONLY

上記コードを下記のように書き換えて実行します。

      // ここを書き換えててテストする
      cl.resolveStrategy = Closure.DELEGATE_ONLY

実行結果です。

before closure
Exception thrown

groovy.lang.MissingMethodException: No signature of method: PipelineSpec.a() is applicable for argument types: () values: []
Possible solutions: b(), c(), any(), tap(groovy.lang.Closure), any(groovy.lang.Closure), is(java.lang.Object)
    at Main$_closureTest_closure1.doCall(closure_01.groovy:25)
    at Main$_closureTest_closure1.doCall(closure_01.groovy)
    // 以下略

a()メソッドが見つからず、MissingMethodExceptionが発生しました。Mainメソッドでの名前解決が行われなかったことがわかります。

DELEGATE_FIRSTとDELEGATE_ONLYの使いどころ

DELEGATE_FIRSTDELEGATE_ONLYは、時に使用者の意図しない挙動を引き起こします。 例えば、以下のように、closureTestメソッド内でpipelineメソッドに渡しているクロージャa()を呼び出したとき、Mainクラスの実装者はMainクラスのa()を呼ぶことを意図していますが、実際にはPipelineSpecクラスのa()メソッドが呼ばれてしまいます。これはバグの温床になりそうです。

class PipelineSpec {
    def a() {
      println "I'm PipelineSpec.a"
    }
}

class Pipeline {
    def pipeline(Closure cl) {
      cl.delegate = new PipelineSpec()
     cl.resolveStrategy = Closure.DELEGATE_FIRST
      println "before closure"
      cl()
      println "after closure"
    }
}

class Main {
    def closureTest() {
        def pipeline = new Pipeline()
        pipeline.pipeline {
            // Main#a()を呼ぶ意図で書いているのに、実際にはPipelineSpec#a()が呼ばれる
            a()
        }
    }

    def a() {
      println "I'm Main.a"
    }
}

def main = new Main()
main.closureTest()

DELEGATE_ONLYの使い所は、DSL(Domain-Specific Language)です。 DELEGATE_ONLYでは、クロージャ内から呼び出せるメソッドを制限することができます。 次回の記事で詳しく解説します。

参考リンク

The Apache Groovy programming language - Closures

環境

次回に続く

yucatio.hatenablog.com

Groovyのクロージャ ②クロージャのthisとownerとdelegate

背景

JenkinsというCIツールではGroovyのコードでCI挙動を記述できるJenkins pipelineがあります。 Jenkins pipelineではGroovyのDSL(Domain-Specific Language:ドメイン固有言語)が使われています。 DSLにはクロージャが効果的に使用されています。

今回はクロージャのthisとownerとdelegateです。

前回の記事

前回の記事で、 クロージャとは生まれ故郷(クロージャ自身が定義された場所)を忘れない無名関数のこと、 クロージャは、クロージャが定義された場所で動く(ように見える)、ということが確認できました。

yucatio.hatenablog.com

クロージャが故郷を忘れないためにしていること

クロージャが実行されたときどのようなことが起こっているのでしょうか。どのようにして元のクラスのメソッドを呼ぶのでしょうか。

その答えは、クロージャが故郷(クロージャが定義された場所)の情報を持っているからです。

クロージャはオブジェクトなので、内部に情報を持てます。クロージャインスタンス変数にはthisownerdelegateの3つが定義されています。

それぞれ以下の情報が入っています。

フィールド 入ってる情報
this クロージャを囲んでいるクラス
owner クロージャを囲んでいるオブジェクト。クラスか、クロージャを囲んでいるクロージャ
delegate 外部のオブジェクト。メソッドやプロパティ名が元のオブジェクトで解決できないときに呼ばれる

確認してみましょう。

class Main {
    // 普通のインスタンスメソッド
    def closureTest() {
        Closure cl = {
          a()
        }
        println "this     : ${cl.thisObject}"
        println "owner    : ${cl.owner}"
        println "delegate : ${cl.delegate}"
    }
    
    def a() {
        println "I'm Main.a"
    }
}

def main = new Main()
main.closureTest()

実行結果です。

this     : Main@2e654c59
owner    : Main@2e654c59
delegate : Main@2e654c59

すべてMainクラスのインスタンスという結果でした。クロージャの中にさらにクロージャがある場合(クロージャがネストしている場合)にはownerとdelegateの値が、対象のクロージャを囲むクロージャになります。詳しくはこちらを参照してください。

qiita.com

ここまでで、クロージャは故郷を忘れないためにthisownerに生まれた場所の情報を格納していることが分かりました。 delegateは、デフォルトではownerと同じですが、クロージャが名前解決に使用するオブジェクトを格納するために使用されます。

delegateにクラスを指定する

delegateにクラスを指定します(正確にいうと、クラスのインスタンスを指定します)。Pipelineクラスのpipeline()メソッド内で、クロージャdelegateを変更します。 PipelineSpecクラスにa()メソッドを定義しました。Mainクラスにはa()メソッドはありません。

class PipelineSpec {
    def a() {
      println "I'm PipelineSpec.a"
    }
}

class Pipeline {
    def pipeline(Closure cl) {
      cl.delegate = new PipelineSpec()
      println "before closure"
      cl()
      println "after closure"
    }
}

class Main {
    def closureTest() {
        def pipeline = new Pipeline()
        pipeline.pipeline {
            a()
        }
    }
}

def main = new Main()
main.closureTest()

実行結果です。

before closure
I'm PipelineSpec.a
after closure

クロージャcl内のa()メソッドは、PipelineSpecのa()メソッドを呼んでいます。 このように、ownerのオブジェクトに該当するメソッドがない場合は、delegateに指定されたオブジェクトのメソッドを実行します。

環境

参考リンク

次回に続く

yucatio.hatenablog.com