lxmlチュートリアル翻訳してみた
PythonでXML(HTML)を扱う高速で便利なライブラリがlxmlです。 非常に強力なメソッドが多数用意されているのですが、日本語の情報があまりないのが弱点です。なので今回、lxml.etree公式チュートリアルの一部を勝手に翻訳しました。
量が多いので全ては訳しきれていませんが*1、lxmlでおそらく一番よく使われるであろう、Elementクラスの部分を中心に訳を作りました。誰かのお役に立てれば幸いです。
また、翻訳にあたっては直訳よりも日本語として自然な訳を選んだ部分があります。正しくは原文を参照してください。ここの訳がおかしい、日本語が不自然、この文書も訳せ等ありましたらお知らせください。
lxml.etreeチュートリアル
これはlxml.etreeを用いたXML処理に関するチュートリアルです。これはElementTree APIのメインコンセプト、およびプログラマとしての生活を楽にするいくつかのシンプルな改善を簡単に見渡すためのものです。 APIの完全なリファレンスについては、生成されたAPIドキュメントをご覧ください。
Contents
lxml.etreeをインポートする一般的な方法は次のとおりです。
>>> from lxml import etree
参照
Elementクラス
ElementはElementTree APIに対する主なコンテナオブジェクトです。ほとんどのXMLツリーは、このクラスを介してアクセスされます。要素はElementファクトリを通して簡単に作成することができます:
>>> root = etree.Element("root")
要素はXMLツリー構造の中にまとめられています。子要素を生成して親要素へ追加するためには、append
メソッドが使えます:
>>> root.append( etree.Element("child1") )
しかし、SubElementファクトリーという、上のことをするよりもずっと簡潔で効率のよい方法があることはよく知られているでしょう。SubElementファクトリーはElementファクトリーと同じ引数を受け取りますが、加えて第1引数として親要素を渡さなければいけません:
>>> child2 = etree.SubElement(root, "child2") >>> child3 = etree.SubElement(root, "child3")
これが確かにXMLであることを確認するためには、作成したツリーをシリアライズします:
>>> print(etree.tostring(root, pretty_print=True)) <root> <child1/> <child2/> <child3/> </root>
要素はリスト
簡単かつ直接これらのサブ要素にアクセスするために、普通のPythonのリストの振る舞いに限りなく近くなるよう要素は定義されています:
>>> child = root[0] >>> print(child.tag) child1 >>> print(len(root)) 3 >>> root.index(root[1]) # lxml.etreeだけ 1 >>> children = list(root) >>> for child in root: ... print(child.tag) child1 child2 child3 >>> root.insert(0, etree.Element("child0")) >>> start = root[:1] >>> end = root[-1:] >>> print(start[0].tag) child0 >>> print(end[0].tag) child3
ElementTree 1.3とlxml 2.0以前は、Elementが子要素を持つかどうかを真偽値で判定できました。すなわち、子要素のリストが空なら、
if root: # 今は廃止されました print("The root element has children")
しかし子要素を持っているかどうかというよりは、存在する「もの」は真と判定されるだろう、Elementは存在する「もの」である(から真と判定される)だろうと考えてしまうという理由で、これは廃止されました。つまりElementが上のような形のどんなif文であっても偽と判定されるのは多くのユーザーを驚かせるでしょうという理由です。その代わり、より明確でエラーの出にくいlen(element)
を使いましょう。
>>> print(etree.iselement(root)) # 何らかのElementであるかどうかを調べる True >>> if len(root): # 子要素をもつかどうかを調べる ... print("The root element has children") The root element has children
lxml(2.0以降)のElementの振る舞いが、リストの振る舞いやオリジナルのElementTree(version 1.3またはPython2.7/3.2以前)の振る舞いとはズレるという別の重要な問題もあります:
>>> for child in root: ... print(child.tag) child0 child1 child2 child3 >>> root[0] = root[-1] # lxml.etreeの要素を代入 >>> for child in root: ... print(child.tag) child3 child1 child2
この例では、最後の要素はコピーされるのではなく別の位置に動いてしまっています。つまり、別の位置に代入したときに以前の位置からは自動的に削除されるのです。リストでは、オブジェクトは同時に別の位置に存在でき、上の場合は単にアイテムの参照を最初の位置にコピーするだけでどちらも完全に同じものとして保持されます:
>>> l = [0, 1, 2, 3] >>> l[0] = l[-1] >>> l [3, 1, 2, 3]
オリジナルのElementTreeでは1つのElementオブジェクトはどのツリーのどの場所にも存在できるので、リストのときと同じコピー操作が発生することに注意して下さい。意図していたとしてもいなかったとしても、このようなElementに関する変更がツリーに現れる全ての場所のElementに適用されることは明らかな欠点です。
この相違点の上に述べた方(Elementオブジェクトの操作の方)によって、Elementがlxml.etree内では常に1つの親要素しかもたないので、getparent
メソッドを使うことができます。これはオリジナルのElementTreeでは実装されていません。
>>> root is root[0].getparent() # lxml.etreeだけ True
lxml.etreeで要素を別の位置へコピーしたいときは、Pythonの標準ライブラリであるcopyモジュールを用いて独立な深いコピーを作成することを考えて下さい:
>>> from copy import deepcopy >>> element = etree.Element("neu") >>> element.append( deepcopy(root[1]) ) >>> print(element[0].tag) child1 >>> print([ c.tag for c in root ]) ['child3', 'child1', 'child2']
要素の親戚(近所)へはnext
やprevious
要素としてアクセスできます:
>>> root[0] is root[1].getprevious() # lxml.etreeだけ True >>> root[1] is root[0].getnext() # lxml.etreeだけ True
要素は属性を辞書としてもつ
XML要素は属性をサポートしています。Elementファクトリー内で直接作成することができます:
>>> root = etree.Element("root", interesting="totally") >>> etree.tostring(root) b'<root interesting="totally"/>'
属性は順序の関係ない名前と値のペアに過ぎないので、要素の辞書のようなインターフェイスを用いると非常に便利です:
>>> print(root.get("interesting")) totally >>> print(root.get("hello")) None >>> root.set("hello", "Huhu") >>> print(root.get("hello")) Huhu >>> etree.tostring(root) b'<root interesting="totally" hello="Huhu"/>' >>> sorted(root.keys()) ['hello', 'interesting'] >>> for name, value in sorted(root.items()): ... print('%s = %r' % (name, value)) hello = 'Huhu' interesting = 'totally'
アイテムを検索したいときや、「本当」の辞書のようなオブジェクトを得たい理由(何かに渡すなど)があるときは、attrib
プロパティを使いましょう:
>>> attributes = root.attrib >>> print(attributes["interesting"]) totally >>> print(attributes.get("no-such-attribute")) None >>> attributes["hello"] = "Guten Tag" >>> print(attributes["hello"]) Guten Tag >>> print(root.get("hello")) Guten Tag
attrib
はElement自身によって提供されている辞書のようなオブジェクトであることに注意して下さい。これはつまりElementを変更するとattribに反映され、逆にattribを変更するとElementにそれが反映されるということです。また、Elementのattribが使われる限りXMLツリーがメモリに残り続けることも意味しています。XMLツリーに依存しない独立した属性を得たい時は辞書へコピーしてください:
>>> d = dict(root.attrib) >>> sorted(d.items()) [('hello', 'Guten Tag'), ('interesting', 'totally')]
要素はテキストをもつ
要素は(プレーンな/XMLのタグとは無関係な)テキストをもっています:
>>> root = etree.Element("root") >>> root.text = "TEXT" >>> print(root.text) TEXT >>> etree.tostring(root) b'<root>TEXT</root>'
たくさんの(データ中心の)XML文書においては、テキストが見られるのはここだけです。ツリー階層の一番下に保持されています*2。
しかし、XMLが(X)HTMLのようなタグづけされたテキスト文書として使われるとき、テキストは別のタグの間、ツリーの途中にも現れるはずです:
<html><body>Hello<br/>World</body></html>
ここでは、<br/>
タグがテキストに囲まれています。これはドキュメントスタイルのXMLやコンテンツが混ざったXMLでしばしばあることです。Elementはtail
プロパティを利用してこれをサポートしています。このプロパティは直接要素に続くテキストをXMLツリーの次の要素のところまで保持しています:
>>> html = etree.Element("html") >>> body = etree.SubElement(html, "body") >>> body.text = "TEXT" >>> etree.tostring(html) b'<html><body>TEXT</body></html>' >>> br = etree.SubElement(body, "br") >>> etree.tostring(html) b'<html><body>TEXT<br/></body></html>' >>> br.tail = "TAIL" >>> etree.tostring(html) b'<html><body>TEXT<br/>TAIL</body></html>'
XML文書のどんなテキストコンテンツを表現するのにも.text
と.tail
の2つのプロパティがあれば十分です。これによって、ElementTree APIはElementクラスに加えて、(古典的なDOM APIで見られるような)よく邪魔になるテキスト用の特別なノードを必要としません。
しかし、tailテキストが邪魔になる場合もあります。例えば、ツリーの中から要素をシリアライズするとき、(子要素のtailテキストが欲しいのであったとしても)結果にtailテキストが必ずしも必要なわけではないでしょう。こういった目的のため、tostring
函数はwith_tail
をキーワード引数としてもちます:
>>> etree.tostring(br) b'<br/>TAIL' >>> etree.tostring(br, with_tail=False) # lxml.etreeだけ b'<br/>'
テキストだけを読みたい時、つまり途中に挟まったどんなタグも必要ないときは、全てのテキストとtail属性を正しい順番で再帰的につなげてやらなくてはいけません。再びtostring
函数が役に立ちます。今回はmethod
キーワードを使いましょう:
>>> etree.tostring(html, method="text") b'TEXTTAIL'
テキスト検索のためのXPath
ツリーのテキストを吐くための別の方法がXPathです。XPathによって、異なるテキストのかたまりをリストへ変換することもできます:
>>> print(html.xpath("string()")) # lxml.etree only! TEXTTAIL >>> print(html.xpath("//text()")) # lxml.etree only! ['TEXT', 'TAIL']
これをよく使うのなら函数にラップしましょう:
>>> build_text_list = etree.XPath("//text()") # lxml.etree only! >>> print(build_text_list(html)) ['TEXT', 'TAIL']
XPathによって返される結果の文字列は特別な「賢い」オブジェクトで、元のオブジェクトのことを知っていることに注意して下さい。getparent
メソッドを使ってElementのときと同じように、元のオブジェクトの情報を得ることができます:
>>> texts = build_text_list(html) >>> print(texts[0]) TEXT >>> parent = texts[0].getparent() >>> print(parent.tag) body >>> print(texts[1]) TAIL >>> print(texts[1].getparent().tag) br You can also find out if it's normal text content or tail text: >>> print(texts[0].is_text) True >>> print(texts[1].is_text) False >>> print(texts[1].is_tail) True
text
函数の結果に対してはこれは期待通りに動く一方、lxmlではstring
やconcat
などのXPath函数によって得られた文字列値の元のオブジェクトは得られない:
>>> stringify = etree.XPath("string()") >>> print(stringify(html)) TEXTTAIL >>> print(stringify(html).getparent()) None
ツリーイテレーション
再帰的にツリーを通過して各要素に対して何かをしたい上のような場合、ツリーイテレーションが非常に便利な解決法です。Elementはこのためのツリーイテレーションを提供します。これは文書の順番、つまりツリーをXMLにシリアライズしたときにタグが現れる順番に要素を出力してくれます:
>>> root = etree.Element("root") >>> etree.SubElement(root, "child").text = "Child 1" >>> etree.SubElement(root, "child").text = "Child 2" >>> etree.SubElement(root, "another").text = "Child 3" >>> print(etree.tostring(root, pretty_print=True)) <root> <child>Child 1</child> <child>Child 2</child> <another>Child 3</another> </root> >>> for element in root.iter(): ... print("%s - %s" % (element.tag, element.text)) root - None child - Child 1 child - Child 2 another - Child 3
もしも1つのタグにだけ興味があるときは、名前をiter
に渡すことでフィルタリングすることができます。lxml 3.0以降では、イテレーション中に複数のタグを取得するために複数のタグを渡すこともできます:
>>> for element in root.iter("child"): ... print("%s - %s" % (element.tag, element.text)) child - Child 1 child - Child 2 >>> for element in root.iter("another", "child"): ... print("%s - %s" % (element.tag, element.text)) child - Child 1 child - Child 2 another - Child 3
デフォルトでは、イテレーションはツリーのXML処理命令((XML文書における<? hogehoge ?>
みたいなやつ))、コメント、エンティティインスタンスを含む全てのノードを吐きます。Elementオブジェクトだけが返ってくることが分かっている場合、Elementファクトリーにtag
パラメータを渡すこともできます:
>>> root.append(etree.Entity("#234")) >>> root.append(etree.Comment("some comment")) >>> for element in root.iter(): ... if isinstance(element.tag, basestring): ... print("%s - %s" % (element.tag, element.text)) ... else: ... print("SPECIAL: %s - %s" % (element, element.text)) root - None child - Child 1 child - Child 2 another - Child 3 SPECIAL: ê - ê SPECIAL: <!--some comment--> - some comment >>> for element in root.iter(tag=etree.Element): ... print("%s - %s" % (element.tag, element.text)) root - None child - Child 1 child - Child 2 another - Child 3 >>> for element in root.iter(tag=etree.Entity): ... print(element.text) ê
タグの名前としてワイルドカード "*" を渡すと全ての要素ノード(そして要素のみ)を吐きます。 lxml.etreeにおいて、要素はツリーのあらゆる方向へのより複雑なイテレーションを提供します。つまり、子要素へ、親要素(やもっと上位の要素)へ、近所へのアクセスができます。
シリアライゼーション
シリアライゼーションには、文字列を返すtostring
函数やファイル・ファイル系オブジェクト・(FTP PUTあるいはHTTP POSTを介した)URLへの書き込みを行うElementTree.write
メソッドがよく使われます。どちらの呼び出しも、整形された出力をするためのpretty_print
とプレーンASCIIではない特定の出力エンコーディングを指定するためのencoding
など、同じキーワード引数を受け付けます:
>>> root = etree.XML('<root><a><b/></a></root>') >>> etree.tostring(root) b'<root><a><b/></a></root>' >>> print(etree.tostring(root, xml_declaration=True)) <?xml version='1.0' encoding='ASCII'?> <root><a><b/></a></root> >>> print(etree.tostring(root, encoding='iso-8859-1')) <?xml version='1.0' encoding='iso-8859-1'?> <root><a><b/></a></root> >>> print(etree.tostring(root, pretty_print=True)) <root> <a> <b/> </a> </root>
pretty printでは最後に改行が追加されることに注意して下さい。
(ElementTree 1.3同様)lxml 2.0以降では、シリアライゼーション函数はXMLシリアライゼーション以外も可能です。method
キーワードを指定することでHTMLへのシリアライズやテキストを吐き出すこともできます:
>>> root = etree.XML( ... '<html><head/><body><p>Hello<br/>World</p></body></html>') >>> etree.tostring(root) # default: method = 'xml' b'<html><head/><body><p>Hello<br/>World</p></body></html>' >>> etree.tostring(root, method='xml') # same as above b'<html><head/><body><p>Hello<br/>World</p></body></html>' >>> etree.tostring(root, method='html') b'<html><head></head><body><p>Hello<br>World</p></body></html>' >>> print(etree.tostring(root, method='html', pretty_print=True)) <html> <head></head> <body><p>Hello<br>World</p></body> </html> >>> etree.tostring(root, method='text') b'HelloWorld'
XMLシリアライゼーションでは、プレーンテキストのデフォルトエンコーディングはASCIIです:
>>> br = next(root.iter('br')) # get first result of iteration >>> br.tail = u'W\xf6rld' >>> etree.tostring(root, method='text') # doctest: +ELLIPSIS Traceback (most recent call last): ... UnicodeEncodeError: 'ascii' codec can't encode character u'\xf6' ... >>> etree.tostring(root, method='text', encoding="UTF-8") b'HelloW\xc3\xb6rld'
バイト文字列の代わりにPythonのunicode文字列へのシリアライズするとより扱いやすいです。unicode型をencoding
へ渡すだけです:
>>> etree.tostring(root, encoding=unicode, method='text') u'HelloW\xf6rld'
参考として、W3CによるUnicode文字列のセットと文字エンコーディングに関する記事もあります。
ElementTreeクラス
ElementTreeは主に、ルートにノードを持つツリーに関するドキュメントラッパーです。これはシリアライゼーションと一般的なドキュメントを扱うためのメソッド群を提供します:
>>> root = etree.XML('''\ ... <?xml version="1.0"?> ... <!DOCTYPE root SYSTEM "test" [ <!ENTITY tasty "parsnips"> ]> ... <root> ... <a>&tasty;</a> ... </root> ... ''') >>> tree = etree.ElementTree(root) >>> print(tree.docinfo.xml_version) 1.0 >>> print(tree.docinfo.doctype) <!DOCTYPE root SYSTEM "test">
ElementTreeはファイルやファイル系オブジェクト(下のパースの節を参照)をパースするためのparse
函数を呼んだときの戻り値でもあります。
重要な違いの一つとして、ElementTreeクラスはElementクラスとは違って完全なドキュメントとしてシリアライズします。これは、文書内のDOCTYPE宣言や他のDTDと同様にXML処理命令やコメントをトップレベルで処理することを含んでいます。
>>> print(etree.tostring(tree)) # lxml 1.3.4 and later <!DOCTYPE root SYSTEM "test" [ <!ENTITY tasty "parsnips"> ]> <root> <a>parsnips</a> </root>
文字列やファイルのパース
lxml.etreeは文字列、ファイル、URL(http/ftp)、ファイル系オブジェクトといったあらゆる重要な形式からXMLのパースを行なう様々な手段をサポートしています。主なパース函数はfromstring
とparse
ですが、どちらもデータソース(上に述べた文字列、ファイル…のこと)を第1引数に渡して呼びます。デフォルトでは標準パーサを用いますが、第2引数としていつでも異なるパーサを渡すことができます。
fromstring
函数
>>> some_xml_data = "<root>data</root>" >>> root = etree.fromstring(some_xml_data) >>> print(root.tag) root >>> etree.tostring(root) b'<root>data</root>'
XML
函数
XML
函数はfromstring
函数のように振る舞いますが、XMLリテラルをデータソースに書き込むためによく使われます。
>>> root = etree.XML("<root>data</root>") >>> print(root.tag) root >>> etree.tostring(root) b'<root>data</root>'
また、HTMLリテラルに対応したHTML
函数もあります。
parse
函数
ファイル系オブジェクトといったものの例のように、以下のコードは外部ファイルの代わりに文字列から読むためBytesIO
クラスを使っています。このクラスはPython2.6以降のioモジュールにあります。より古いバージョンのPythonでは、StringIOモジュールのStringIO
クラスを使わなければいけません。しかし実際、明らかにこれらを一緒に使うことは避け、上の文字列パース函数を使うべきでしょう。
>>> some_file_like_object = BytesIO("<root>data</root>") >>> tree = etree.parse(some_file_like_object) >>> etree.tostring(tree) b'<root>data</root>'
parse
は文字列パース函数のときのようなElementオブジェクトではなくElementTreeオブジェクトを返すことに注意して下さい。
>>> root = tree.getroot() >>> print(root.tag) root >>> etree.tostring(root) b'<root>data</root>'
この違いの裏にはparse
がファイルから完全な文書を返すのに対し文字列パース函数は普通は断片的なXMLをパースするのに使われるというわけがあります。
parse
函数は以下のデータソース全てをサポートしています:
- openファイルオブジェクト(バイナリモードで開いていることを確認して下さい)
- 呼ばれる度にバイト文字列を返す
.read(byte_count)
メソッドを持ったファイル系オブジェクト - ファイル名文字列
- HTTPまたはFTPのURL文字列
ファイル名やURLを渡すのは、オープンファイルやファイル系オブジェクトを渡すよりも速いことが多いことに注意して下さい。しかし、libxml2
のHTTP/FTPクライアントはかなり単純なので、HTTP認証などはURLリクエスト専用のurllib2
やrequest
といったライブラリが必要です。これらのライブラリは通常、responseが返ってきている間にパース可能なファイル系オブジェクトを結果として提供します。
パーサオブジェクト
インクリメンタルパース
イベントドリブンパース
名前空間
E-ファクトリー
ElementPath
ElementTreeライブラリはシンプルなXPathに近い、ElementPathと呼ばれるパス言語を備えています。最大の違いは、ElementPathの表記では{namespace}
タグ記法が使えることでしょう。しかし、値の比較や函数といった高度な機能は使えません。
完全なXPathの実装に加えて、lxml.etreeは(ほとんど)同じ実装を使っているとはいえElementTreeと同じ方法でElementPath言語のサポートもしています。APIはElementとElementTreeで、以下の4つのメソッドを提供しています:
iterfind
はパス条件にマッチする全ての要素をイテレートしますfindall
はマッチした要素のリストを返しますfind
は最初にマッチしたものを効率良く返しますfindtext
は最初にマッチしたものの.text
の内容を返します
以下に例をいくつか示します。
>>> root = etree.XML("<root><a x='123'>aText<b/><c/><b/></a></root>")
要素の子要素を探す:
>>> print(root.find("b")) None >>> print(root.find("a").tag) a
ツリー内の全ての要素を探す:
>>> print(root.find(".//b").tag) b >>> [ b.tag for b in root.iterfind(".//b") ] ['b', 'b']
特定の属性を持つ要素を探す:
>>> print(root.findall(".//a[@x]")[0].tag) a >>> print(root.findall(".//a[@y]")) []
.iter
メソッドはツリー内の特定のタグを名前を使って探すだけで、パスによってではありません。これはつまり以下のコマンドは成功する場合は等価になります:
>>> print(root.find(".//b").tag) b >>> print(next(root.iterfind(".//b")).tag) b >>> print(next(root.iter("b")).tag) b
.find
メソッドは何にもマッチするものがなければNone
を返すだけだが、他の2つの例はStopIteration
エラーを返すことに注意して下さい。
ubuntuで音が出なくなった
ubuntuを使っていたら突然音が出なくなりました。
結論から言うとpulseaudioとflashplugin-nonfree-extrasoundをインストールしなおしただけです。
環境
- OS: ubuntu 12.04 LTS
- 問題のあったGoogle Chrome: version 28
経緯
最近Google Chromeで``Shockwave Flash has crashed"のメッセージがよく出るようになりました。
(Youtube等は問題なく見れていましたが、ブログパーツ等でFlashプラグインを利用するものがクラッシュするという感じです。)
ググれば情報はたくさん出てきて、「chrome://pluginsから2つあるFlash pluginのどちらかを無効にする」というのが一般的な解決法のようですが私の場合はそれでは解決しませんでした。
そしてChromeを色々いじっていると、突然ubuntuそのものから音が出なくなり、通常の動画再生などをしても何も聞こえなくなりました。
音量調節等は上手くできていて、pulseaudioも音を拾っていて、困っていたのですが…。
解決方法
/var/log/syslogにulseaudio[29142]: module-alsa-sink.c: Error opening PCM device front:0: Device or resource busy
というエラーがあり、これをそのままググったら以下のサイトが。
module-alsa-sink.c: Error opening PCM device front:0: Device or resource busy | Island Linux
ここに書いてあるとおりに
~$ sudo apt-get install --reinstall pulseaudio flashplugin-nonfree-extrasound
を実行した後、システム > 設定 > サウンド
でサウンドを設定しなおしたら音が出るようになりました*1。
ついでに、Flashのクラッシュもなくなったような気がするので、同じようにubuntuでクラッシュする方はやってみてもいいかもしれないです。
*1:libflashsupportはflashplugin-nonfree-extrasoundに置き換えられているらしいのでこっちを入れました
HaskellのMonadは何をしているか
はじめに:モナドが何かわからないけど分かりたい人へ
この記事はあなたの為に書かれたものです! 「モナドって難しいそうだしよく分からない、けどきっと賢い人たちが『モナドとは何か』について素敵な説明を与えてくれるはず!」 …そういう期待を持っているまさにあなたのその勘違いを是正するためにこの記事はあります。
「モナドが何か」を理解するのはまずは諦めて下さい。
この記事は、世界一わかりやすいモナドの記事 - みょんさんの。を理解したいと思わない人のために、プログラミングの文脈でモナドに関する少しの考え方を提供するものだと考えて下さい*1。
what it is から how it works へ
モナドはモノではありません。だから「何」という質問に答えるのは難しいでしょう。数学的な公理を読者に投げつけることも意味不明な喩えでごまかすこともしたくはないので、少しだけ違う説明をします。
例を挙げましょう。
class Eq a where (==), (/=) :: a -> a -> Bool x /= y = not (x == y) x == y = not (x /= y)
さてこれはPreludeにあるEq型クラスの定義です。Eqは函数(==)の実装のみを与えればそれだけでインスタンスにすることができます。
私達が普段やるように、自分で定義したデータ型の同値性を定義してみましょう。
data Hoge = Fuga | Piyo instance Eq Hoge where Fuga == Fuga = True Fuga == Piyo = False Piyo == Fuga = False Piyo == Piyo = True -- Fuga /= Piyo -- = not (Fuga == Piyo) <- Eqにおける(/=)の定義より -- = not (False) <- instance Eq Hogeの定義より -- = True
しかしこれは以下にように書いても全く問題ありません。
data Hoge = Fuga | Piyo instance Eq Hoge where Fuga == Fuga = False -- <- ??? Fuga == Piyo = True -- <- ??? Piyo == Fuga = False Piyo == Piyo = True
インスタンス宣言の上2行は、明らかに私達が知っているような同値性とは異なります。しかしこれは全く正しいのです。
なぜなら、「Eqとは何が正しいのかを知っているのではなくて、何を正しいとみなしてプログラムを動かすか」を定義するからです。
Eqとは同値性を与える型クラスである、というのは誤解を招く表現です。上のように私達の知っている同値性とは異なるものがEqのインスタンスになる場合があるからです。以下では「Eqとは何を同値とみなすか」を与える型クラスであると考えて下さい。
この、what it isではなくてhow it worksという考え方は型クラスについて理解するのに便利なことが多々あります。
how Functor works
Eq型クラスは何を同値とみなすかを与えるような型クラスでした。ではFunctorとは何でしょうか?
Functorは、「どうやって函数を適用するか」を与えるような型クラスだと思って下さい。例を見ましょう。
class Functor f where fmap :: (a -> b) -> f a -> f b -- 「どうやって函数を適用するか」をfmapの実装で与える data Identity a = Identity a -- Identityの定義(実際は何もしない) instance Functor Identity where fmap f (Identity a) = Identity (f a) -- Identity aに対してはaにそのまま函数fを適用させる -- fmap (2+) (Identity 3) == Identity (2+3) data Maybe a = Nothing | Just a -- Maybeの定義 instance Functor Maybe where fmap _ Nothing = Nothing -- Nothingに対してはNothing fmap f (Just a) = Just (f a) -- Just aに対してはaにfをそのまま適用させる data List a = Cons a (List a) | Nil -- Listの定義(Listを[]に読み替えて下さい) instance Functor [] where fmap = map -- Listに対しては、mapという高階函数を使って函数を適用する instance Functor ((,) a) where fmap f (x,y) = (x, f y) -- 今aを固定したとき、(a,b)に対してはbに対してfを適用させる -- fmap (2+) (4,5) == (4, 2+5)
Functorというのは函数を適用させる方法を与えるような型クラスです。これは函数を適用して計算をさせるときに、どうやって函数の適用という操作をするかを定義しているのです。
実は、Monadも全く同じ事です。
how Monad works
Monadは、「どうやって計算に変換・計算を連結するかを与えるような型クラス」だと思ってみましょう。
class Monad m where return :: a -> m a -- ただの値aを(m a)という「計算(の結果)に」変換する (>>=) :: forall a b. m a -> (a -> m b) -> m b -- Aという計算結果とBという計算(をする函数)の合成をA >>= Bで与える instance Monad Identity where return a = Identity a -- ただの値aを(Identity a)という計算の結果に変換 (Identity a) >>= f = f a -- Identity aという計算の結果と計算fを合成(そのまま適用させるだけ) instance Monad Maybe where return a = Just a -- ただの値aを(Just a)という計算の結果に変換 (Just x) >>= k = k x -- Just xという結果(この場合は計算が「成功した」・「意味を持っている」)とkの計算をそのまま適用させて合成 Nothing >>= _ = Nothing -- Nothingという結果は何を合成してもNothing -- Just 9 >>= (\x -> Just (2+x)) == Just (2+9) -- Nothing >>= (\x -> Just (2+x)) == Nothing instance Monad [] where return x = [x] -- ただの値を[x]という計算の結果に変換 m >>= k = foldr ((++) . k) [] m -- リストの全要素を函数に適用させたものを結合し、それを計算の結果とする -- [1,2,3] >>= (\x -> [x+7, x+9]) == [1+7, 1+9] ++ [2+7, 2+9] ++ [3+7, 3+9] == [1+7, 1+9, 2+7, 2+9, 3+7, 3+9]
有名な例をいくつか挙げてみました。どれも、少し手を動かしてみると「どうやって計算に変換・計算を連結するかを与えている」ことが分かると思います。
さて、実はMonadで出てきた「計算」というのは何も数の計算には限りません。そもそも、「プログラム自体」計算の一種です。
-- Free Monad -- from http://hackage.haskell.org/packages/archive/free/3.4.2/doc/html/Control-Monad-Free.html#t:Free data Free f a = Pure a | Free (f (Free f a)) instance Functor f => Functor (Free f) where fmap f (Pure a) = Pure (f a) -- データ(Pure a)にはaにfを適用させてPure (f a)とする fmap f (Free fa) = Free (fmap (fmap f) fa) -- データ(Free fa)に対してはfをfmap (fmap f)としてfaに適用させる(これは再帰的な構造になっているので注意して下さい。faは(f (Free f a))という部分からなります) instance Functor f => Monad (Free f) where return = Pure -- ただの値をPureで包んでFree型に変換する Pure a >>= f = f a -- Pure aとfの計算の合成はf a Free m >>= f = Free (fmap (>>= f) m) -- Free mとfの計算の合成はfmap (>>= f)をmに適用させることで得られる
これが実際にプログラムのように動くことはFree Monadを使ってみよう!(2) - みょんさんの。でも説明しました。プログラム自体を(当然Input/Outputも含めて、です!画面に``hello!"と表示することも計算の一部、あるいは結果と見ているのです!)計算と考えれば上のようなものも作れます*2。
あるいは、「副作用のある値」を計算の結果と考えましょう。つまり、例えばキーボードからの入力によって得られるような、プログラムの中からは知りえない値は、外の世界で計算された結果であると考えます。するとこのMonadは「外の世界からの値を扱う計算の合成を与える」ことになります。
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #)) -- IO a:外の世界からの値 -- (a -> IO b):ただの値aを外の世界からの値IO bに変える「計算」 -- IO b:IO aと(a -> IO b)の計算の合成の結果 (>>=) :: IO a -> (a -> IO b) -> IO b
これはIOモナドと呼ばれ、このモナドのおかげでHaskellは副作用を扱うのがとても簡単です。常にIOはモナドとしてしか表れないので、普通の処理は普通に行い、IOが絡むところだけはモナドを処理するための函数を書いてやることで純粋な部分と純粋でない部分(IOが混ざってくる部分)を綺麗に分離することができるのです。
そういえばMonadとは何だったのか
Monadが何かという説明は一切なしにここまで説明してきました。Monadは実は無条件になんでもMonadにしてよいのではなくて、ある条件を満たさなければなりません。
この法則のことをMonad則といいます。
return a >>= k == k a m >>= return == m m >>= (\x -> k x >>= h) == (m >>= k) >>= h
上に登場したものは全て(Identity, Maybe, List, Free, IO)この条件を満たしています。
しかしこれはただのMonadが満たすべき規則であって、Monadそれ自体ではありません。Monadとは何だったかというと、「計算への変換と合成を与える型クラス」でした。上の条件を少し眺めてみて下さい。
-- Maybeの例 Just a >>= k == k a Just a >>= Just == Just a Nothing >>= Just == Nothing Just a >>= (\x -> k x >>= h) == k a >>= h == (Just a >>= k) >>= h Nothing >>= (\x -> k x >>= h) == Nothing == (Nothing >>= k) >>= h
もちろん成り立っていますね。これが「明らか」に見えたら相当Monadに慣れている証拠です!
そもそもMonadというのは全然特殊なものではありません。その証拠に、いたる所にMonadはあります。なにせ「Free Monad」は全てのFunctorを無条件でMonadにできるくらいですから!Functorは「函数をどうやって適用させるか」を決めているだけなので、データ型の数だけ無数にあります。それらが全てMonadとして機能するわけです。
実際のプログラムの中ではIOモナドやOperational Monadと行った「便利で」「革命的な」モナドがたくさん使われます。しかし根本的には全て「計算への変換と合成」を定義しているに過ぎません。そしてそれらは上の関係を満たしている、ということです。IOモナドは外の世界の値を計算の結果に、そしてOperational Monadは(構造としてはFree Monadの構造と同じです)プログラムそれ自体を計算の結果と見ているということです。
あなたの周りにはたくさんのMonadがあります。そしてそれらを簡単に作ったり扱うための函数がすでにたくさん揃っています。あとは使いこなすだけですね。
それではHappy Monad Life!
補足:Free Functor
Free Functor Fは左随伴で、Forgetful Functor Uという右随伴をもちます。すなわち、とすると
という集合としての同型があって、かつc,dのどちらについてもnaturalです*3。
さらにFree Functorというのは`Free'、すなわち特にこれと言って条件を必要とせずに「自由な構造」を作り出します。自由な構造というのは、ここではモナドの圏になる(各対象がモナド、射がモナド準同型なる圏)という構造です。
Free Functorとは与えられたFunctorの圏からMonadの圏への自由函手というのは上のような意味です。Freeが本質的なのはその存在が函手圏の構造には依存しないところで、つまりMonadの中でも最も一般的なもの、あるいは最も小さい構造を持ったもの、という意味でぜひ理解して欲しい一例だったということでした。