お布団宇宙ねこ

にゃーん

VRoidの3Dモデルに目の瞬きを覚えさせる

VRoid製の3Dモデルは瞬きのやり方を知らないので今回はこれを覚えさせます。

瞬きの方法については検索すればいくらか情報が出てきますが、VRoidの場合は少し方法が異なります。

「シェイプキー」を作る必要はない

「シェイプキー」とはオブジェクトの形を記憶させておく機能のことです。「モーフ」とも呼ばれます。

人の3Dモデルを例に挙げると、真顔や笑顔などの表情やサムズアップのような手のジェスチャーをシェイプキーとしてよく作ります。このシェイプキーを使ってキーフレームで真顔から笑顔に、平手からサムズアップに変化させるようにアニメーションを作ることができます。

自作アバターを一から作る場合はBlender等で適宜シェイプキーを作る必要がありますが、VRoidの場合はデフォルトでいくつかシェイプキーが登録されているのでそれらを使ってある程度の表情などが出せるようになっています。

mtprince9 さんがVRoidで出力される表情のリストを貼ってくれているのでこちらを参考にしましょう。

この結論に辿り着くまでだいぶ時間がかかりました..。

FBX形式でエクスポートする

VRoidの3Dモデルのシェイプキーを利用するために、FBX形式でエクスポートするところまでやりましょう。一つ注意点としては、FBXでエクスポートする際のオプションで「Animated Skinned Mesh」にチェックを入れてエクスポートする必要があります。

FBX形式でのエクスポート方法については以下の記事で書いたので参考までにどうぞ。

ku00.hatenablog.com

FBXのエクスポートが完了してFBXファイルをHierarchyに追加したら、UMARendererを選択し[Inspector]->[Skinned Mesh Renderer]->[BlendShapes]が追加されていることを確認してください。

f:id:ku00:20180827000054p:plain

登録されているシェイプキーは0~100の値で調整できます。 試しに1つ値を変えてみるとこんな感じになります。

f:id:ku00:20180827000100p:plain f:id:ku00:20180827000104p:plain

瞬きのアニメーションを作る

瞬きに合うシェイプキーを探しましょう。 目を閉じる Face.M_F00_000_Fcl_EYE_Close が最適そうですね。

アニメーションの作り方については文章で説明するのが大変なので以下の動画を参考にしてください(本当に動画の方が分かりやすいので..!)

youtu.be

瞬きができるようになったらVRChatにアップロードしてみましょう。

目の瞬きがあるだけでも人間らしくなりますね。それでは。

VRoidの3DモデルでVRChatの世界に入ったよ

先日ようやくVRChatのアップロード制限が解除されたので、今回は前回の記事で作ったVRoidの3Dモデルを使ってVRChatの世界に入ろうと思います。

こちらの記事を参考にさせていただきました。ありがとうございました。
VRoidで作成したモデルをVRChatの世界に送り出す方法 - あたまのたいそう

下準備

事前に用意するものがいくつかあるので、まずは下準備からです。
順番に説明していきます。

  • 3Dモデルのポリゴン数を2万以内に収める
  • VRoidから作成した3DモデルをVRM形式でエクスポートする
  • 必要なソフトウェアやファイルを揃える

3Dモデルのポリゴン数を2万以内に収める

VRChatに持ち込める3Dモデルはポリゴン数が2万までと制限されているため、この閾値を超えないように3Dモデルを作っておく必要があります。VRoidでは意識せずにアバターを作っていると軽く閾値を超えてしまうため注意が必要です。

ポリゴン数を削減する方法については前回の記事で紹介しているのでここでは触れません。

また、Simplygonを利用してポリゴン数を削減できるようですが、自分の場合はこの方法ではうまくいかなかったのでやめました。(テクスチャが参照できないみたいなエラーが発生してしまった)

VRoidから作成した3DモデルをVRM形式でエクスポートする

ポリゴン数が閾値以下で収まったら、VRM形式でエクスポートしましょう。

VRoidの上部タブの [撮影・エクスポート] から左メニューの [エクスポート] を選択、エクスポートに必要なアバターの情報を入力して [エクスポート] を押下するだけです。

必要なソフトウェアやパッケージを揃える

最後の下準備として、今後の作業に必要なソフトウェアとパッケージを用意しましょう。必要なものは以下です。

Unityはダウンロード後にインストールまで行ってください。複数バージョンインストールするのでインストールするディレクトリをそれぞれ区別できるように変更してください。

https://docs.unity3d.com/jp/460/Manual/InstallingMultipleVersionsofUnity.html

Unity Packageはこの時点ではダウンロードのみでOKです。

Unityで2バージョン必要なのは、前者の 2.3f1 をVRM形式をFBX形式にコンバートするために利用し、後者の 5.6.3p1 をVRChatへアップロードするために利用するからです。

VRCSDKはVRChatにログインして、CONTENT CREATIONの [DOWNLOAD SDK] からダウンロードできます。

CubedsShadersはシェーダーなので必要のない人はスキップしてください。

以上で下準備は完了です。


やること

ここからようやく作業に入ります。やることは以下です。

  • VRM形式の3DモデルをFBX形式にコンバートする
  • VRChat用に3Dモデルを整える
  • VRChatにアップロードする

VRM形式の3DモデルをFBX形式にコンバートする

Unity 2.3f1を起動してプロジェクトを新規作成します。

作成できたらVRM形式のファイルを読み込めるようにするためにUniVRMをインポートします。

[Assets] -> [Import Package] -> [Custom Package] から、UniVRM-0.40.unitypackageを選択してください。

パッケージのインポートが終わったらVRMファイルを下の画像の枠線内にドラック&ドロップで渡すと読み込んでくれます。

f:id:ku00:20180816235634p:plain

読み込みが完了するとprefabファイルとそれに付属するテクスチャなどのディレクトリがプロジェクトのAssetsディレクトリ以下に作成されます。


次に読み込んだVRMファイルをFBX形式にコンバートします。

コンバートにはFBX ExporterというAsset Packageを利用します。
FBX ExporterはAsset Store経由でインポートすることができます。

真ん中の画面の上部タブから [Asset Store] を押下し、そこからストアにアクセスできます。

f:id:ku00:20180816235638p:plain

パッケージのインポートが完了したら、先ほど読み込んだVRMファイル、正確には.prefabという拡張子が付いているファイルを左側のHierarchyにドラック&ドロップします。

f:id:ku00:20180816235641p:plain

それができたら右クリックメニューから [Export To FBX] を選択します。

Export Optionsはデフォルトのままで [Export] を押下します。

エクスポートが完了するとAssetsディレクトリ以下に.fbxのファイルが作成されているはずです。

Unity 2.3f1の役目はここで終わりなので終了してください。

VRChat用に3Dモデルを整える

ここからはUnity 5.6.3p1を使います。

別バージョンのUnityなので、プロジェクトは先ほどのを使いまわすのではなく新規作成してください。

作成できたらコンバートのときと同じ要領で以下のパッケージをインポートします。

  • UniVRM
  • VRCSDK
  • CubedsShaders

インポートが完了したらVRMファイルとFBXファイルの順番でAssetsにドラック&ドロップします。なぜコンバートしたのにVRMファイルも一緒に読み込むのかというと、FBXファイルにはテクスチャの情報が付属しないためVRMのものを参照する必要があるからです。

その後、FBXファイルを左側の Hierarchy にドラック&ドロップすることで3Dモデルを整える準備が完了します。


まずHierarchyに追加したFBXの階層からUMARendererを選択します。

f:id:ku00:20180817012352p:plain

選択すると右側の [Inspector] にマテリアルの情報が出るので、ここから各マテリアルのシェーダーを CubedsShaders のに変更します。CubedsShaders にも種類があり自分は Flat Lit Toon にしました。

f:id:ku00:20180817012402p:plain

FBXファイルをインポートした時点ではマテリアルの一部が意図せず黒くなって表示されることがありますが、これは Rendering Mode を [Transparent] などに変更すると直ります。

f:id:ku00:20180817012417p:plain

なんかパンダみたいになる..

f:id:ku00:20180817012409p:plain

直った!

f:id:ku00:20180817012652p:plain


次はボーンの設定です。

Assets内のFBXファイルを選択し、右側の [Inspector] -> [Rig] の Animation Type を [Humanoid] に変更し、 [Apply] を押下します。

f:id:ku00:20180817022453p:plain

先ほどと同じく右側の [Inspector] -> [Rig] の Avatar Definition の [Configure] を押下します。

ボーン設定に遷移するので、Bodyを以下のように変更します。

  • Chest: [J_Bip_C_UpperChest]
  • UpperChest: [None]

f:id:ku00:20180817022506p:plain

変更したら [Apply] 、[Done] の順で押下したら完了です。 これでモデルデータが人型(Humanoid)として認識されます。


左側のHierarchyのモデルの [Inspector] から [Add Component] を押下して [Scripts] -> [VRCSDK2] -> [VRC Avatar Descriptor] を選択します。

[Inspector] に VRC_Avatar Descriptor が追加されるので、まずは Default Animation Set を [Famale] に変更します。

次に View Position を変更します。これはアバターを使用したときの利用者の目線を表しています。[Scene] の画面にグレー色の球体がデフォルトだとモデルの真上くらいに表示されるので、これを目と目の間にめり込むくらいの位置に調整します。

f:id:ku00:20180817022519p:plain

参考までに自分の設定はこちらになります。

f:id:ku00:20180817022533p:plain

VRC_Avatar Descriptorの詳細な設定についてはVRChat 日本wikiにも書かれているのでそちらも参考までに。

ここまでくればあとはVRChatにアップロードするだけです。

VRChatにアップロードする

まず先に上部メニューの [VRChatSDK] -> [Settings] からVRChatにログインします。

次に [VRChatSDK] -> [Show Build Control Panel] から [Build & Publish] を押下します。

処理が完了すると真ん中の画面にVRChatのアップロード画面が表示されるので、各項目を入力してから[Upload]ボタンを押下すれば完了です。

f:id:ku00:20180817024324p:plain

あとはVRChatを起動して設定画面の [AVATAR] にアップロードしたアバターが表示されていればOKです。

お疲れさまでした。

(目のハイライトが消えてしまったのであとで直す..)

VRoidで美少女アバターを作ったよ

vroid.pixiv.net

pixivからVRoid Studioという人型アバターの3Dモデルが簡単に作れるアプリケーションがリリースされましたね。

今回はVカツで作ったアバターをVRoidでも再現できるのかどうか試してみました。

結論から言うと作ることはできましたがVカツのようにパラメータのみをいじって完成!とはいかず、絵(イラスト)を描くのと同じような作業がいくつか必要になり慣れるまではちょっと大変かなという印象でした。

チュートリアルや操作説明などのドキュメントについてはベータ版ということもあってか整っていません(というよりもほぼ無いに等しい)。 また、VRoidハッシュタグで検索したりVRoidの公式TwitterのRTを覗いてみれば他の方のアバターがたくさん見られますが、ざっと探した感じだとドキュメントを補完するような情報は少なかったです。

なので自分が作るときに参考にしたものや使ったものなどをメモ程度に紹介します。

作ったアバターをVカツのと比較してみる

どんな感じのアバターができるのかをまず先に載せます。

Vカツのりたろうちゃんがこちら

今回作ったVRoidのりたろうちゃんはこちら

髪の色味・光具合とか目の大きさとか色々違うけどこれはこれでいいよね

作り方の雰囲気を掴むために動画を観る

公式でチュートリアル動画などがあればよかったのですが、無いので適当にYouTubeで検索しました。自分が参考にしたのはこの動画です。

VRoid Studioでレム(remu)作製 裏技公開の制作流れ VRoidStudio髪編集と顔編集のまとめ先行制作 - YouTube

VRoidを最初に開いたときにパッと見て操作方法がわからないという理由から触るのをちょっとためらってしまいました。

参考動画では目や髪の毛の描き方を中心とした作業の様子を映したものなので、この操作をするとこう変わるみたい直感的に理解できてよかったです。

ペンタブがあると描きやすい

マウスでも描けないことはないと思いますが、特に髪は手のブレが結構反映されてしまうのでペンタブがあると線が真っ直ぐ描けると思います。

筆圧についてはまだ対応してなかったと思います。

参考までに自分はWacomの『One by Wacom』を使っています。

store.wacom.jp

ポリゴン数を減らしたいときは髪の毛を調整するといい

こちらの方のツイートで言及されている通り、VRChatではアバターのポリゴン数が2万ポリと制限されているためそういったサービスで利用する場合は調整が必要になります。

自分は最終的にVRoidで作ったアバターをVRChatに持っていきたい気持ちがあったので同じような方法で調整しました。

調整前は約26000ポリでしたが、「滑らかさ」を前髪以外 40.0 から 15.0 に、「断面形状」をひし形から底なし三角形に変更することで約17000ポリまで下げることができました。

前髪の滑らかさを変更しなかったのは、滑らかさを削ると思いのほか粗さが目立ってしまったからです。

衣装はどう描けばいいんだ..

アイラインや衣装などをどうやって描くのかについてはよくわからないままでした。前者はモデルのプレビューを見ながら雰囲気で描けましたが、後者は雰囲気では描けそうになかったので諦めました(衣装の展開画像を見てもこれでどう描けと..)

f:id:ku00:20180815140630p:plain
衣装のテクスチャ画面

VRoidへのフィードバック

  • [既知] vroidファイルを読み直したときに発生する髪の毛が意図しない位置に吹っ飛ぶ不具合を早めに直してほしいです
    • 高さ調整とかでは修正が効かなくて結局描き直しました😢
  • 髪の毛の流れを調整するプレビュー側のガイド(?)のみのundoができるようになってほしい
    • ガイドが変になったと思って直そうにも直せないので(そしてやり直すために読み込み直しをして上の不具合にぶち当たるコンボがつらい)
  • 体型編集の身長調整で実身長の目安が出ると嬉しい

まとめ

冒頭にも書いた通りドキュメント不足による障壁は大きいですが、慣れてしまえばのは難しくないと思います。描き始めてからはそこまでつっかかることはなく完成させることができました。これは自分がVRoidを立ち上げたときのデフォルトのモデルから主に目と髪の毛のみを描き直すという方法を取れたことも大きな要因だと思っています。

ひとまず絵がほとんど描けない自分でもそれなりのクオリティの3Dモデルが作れるのでVRoidさん本当にありがとうございますという気持ちです。

皆さんもぜひ試してみてください。それではよいバーチャルライフを。

optparse-applicativeを使ってCUIツールを作る

Haskellで簡単でもいいから何かツール作りたいなあと思ってシュッと作れそうなCUIツールを作ったので、作るにあたって利用したライブラリoptparse-applicativeの紹介をします。

はじめに

今回CUIツールを作る上で基本となるオプションパーサの部分にライブラリを使いました(というよりそこにしか使いませんでした)。オプションパーサのライブラリはこちらを見つつ、その後optparse-applicativeoptparse-simpleoptparse-genericに絞って、最終的に一番ドキュメントの量が多くコードの読みやすかったoptparse-applicativeを使っています。optparse-genericもよかったのですがサブコマンドの書き方がよくわからなくて断念してしまいました。

オプションパーサのライブラリを使ってますが、実はオプションは一つも実装していないのでどちらかと言うとサブコマンドを実装したい人向けの記事になります。

実装例

コマンド部分は以下のように簡単に作れます。

module Main where

import Options.Applicative
import Data.Semigroup ((<>))

data Command
  = Hello name
  | Bye name msg
  deriving (Show)

parseHello :: Parser Command
parseHello = Hello <$> argument str (metavar "[NAME]")

parseBye :: Parser Command
parseBye = Bye
  <$> argument str (metavar "[NAME]")
  <*> argument str (metavar "[MESSAGE]")

withInfo :: Parser a -> String -> ParserInfo a
withInfo opts desc = info (helper <*> opts) $ progDesc desc

parseCommand :: Parser Command
parseCommand = subparser $
  command "hello" (parseHello `withInfo` "Say Hello") <>
  command "bye" (parseBye `withInfo` "Say Bye")

parseInfo :: ParserInfo Command
parseInfo = parseCommand `withInfo` "Greeting"

execCommand :: IO ()
execCommand = execParser parseInfo >>= run
  where run cmd = case cmd of
                    Hello n -> sayHello n
                    Bye n m -> sayBye n m

main :: IO ()
main = execCommand

まず、実装したいサブコマンドやオプションの型を定義してそれをParser型で包んだパーサ関数を定義します。ここではComannd型を定義してparseHelloparseByeを実装しています。このパーサ関数にはコマンドが受け取る引数やヘルプで表示したい情報などを設定できます。

次にそれらのパーサ関数をまとめるパーサを用意します。ここではparseCommandがそれにあたります。

あとはそれをexecParser関数に食わせることで入力された文字をいい感じにパースしてコマンドと引数に分解してくれます。

実行

デフォルトでヘルプオプションが付いてくるので、上のような実装だけでもいい感じのコマンドが作れてしまいます。

> stack runghc Main.hs -- --help
Usage: Main.hs COMMAND
  Greeting

Available options:
  -h,--help                Show this help text

Available commands:
  hello                    Say Hello
  bye                      Say Bye

> stack runghc Main.hs -- hello --help
Usage: Main.hs hello [NAME]
  Say Hello

Available options:
  -h,--help                Show this help text

まとめ

以上、optparse-applicativeを使うとお手軽にCUIツールを作れるのでした。
自分が実際に作ったものはこちらに置いてあります。

github.com

参考文献

Applicativeを理解する

前回の記事ではFunctorについて紹介しました。今回はFunctorをさらに強力にしたApplicativeという型クラスについて解説します。

適用する関数も箱に入れる

Functorでは、ある状態という箱に入った値を関数に適用するための振る舞いを定義しているのでした。

ここで適用する関数も箱に入っていた場合を考えてみましょう。当然fmapでは箱に入った関数を取り出すことはできません。

> fmap (Just (+3)) (Just 3)
<interactive>:44:7:
    Couldn't match expected type ‘Integer -> b’
    ...

そこで登場するのがApplicativeです。Appliactiveは箱に入った値を箱に入った関数に適用する方法を知っています。

Applicativeとは

ApplicativeはControl.Applicativeに定義されています。

class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

http://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#Applicative

Applicativeには型制約が付いておりApplicativeのインスタンスになるためにはFunctorのインスタンスである必要があります。

今回、箱に入った値を箱に入った関数に適用するには(<*>)を使います。この関数はfmapと型シグネチャが似ていますが、第一引数の関数がアプリカティブ値に入っています。Functorと同じくこの値には箱となる型が入ります。

では、Maybeを例に(<*>)がどのような実装になっているのか見てみましょう。

instance Applicative Maybe where
  pure = Just
  Just f <*> m = fmap f m
  Nothing <*> _m = Nothing

http://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-656

実際に使ってみます。

> Just (+3) <*> Just 3
Just 6
> Nothing <*> Just 3
Nothing

Justに入った関数がきたときには、箱から関数を取り出してfmapを使って値に適用しています。値を関数に適用した後の箱に値を入れ直す処理はfmapを使えばFunctorが全てやってくれます。

さらに<*>は連続して組み合わせることもできるので、はじめから箱に関数が入っていなくても計算することができます。pure関数を使って関数をアプリカティブ値で包むことで実現できます。

> :t pure (+) <*> Just 100
pure (+) <*> Just 100 :: Num a => Maybe (a -> a)
> pure (+) <*> Just 100 <*> Just 3
Just 103

けれど<$>を使うことでもっと簡単に書くことができます。

> :t (+) <$> Just 100
(+) <$> Just 100 :: Num a => Maybe (a -> a)
> (+) <$> Just 100 <*> Just 3
Just 103

それと同じ処理をする便利な関数liftA2もあります。

> :m Control.Applicative
> liftA2 (+) (Just 100) (Just 3)
Just 103

Applicativeのインスタンス

Functorのインスタンスである型ならどれでもApplicativeになれます。
その中でもリストは変わった結果を返します。

> [(^2),(+100)] <*> [1..5]
[1,4,9,16,25,101,102,103,104,105]

リストアプリカティブファンクターは適用する関数が複数である可能性があるため、その全ての関数に対してそれぞれの値を適用した新しいリストを返します。この例では2*5=10通りの組み合わせのリストが返ってきます。このようにリストは複数の結果を取り得る非決定性計算を表現するのに使われます。

インスタンス定義は次のようになっています。<*>はリスト内包表記で実装されています。

instance Applicative [] where
  pure x = [x]
  fs <*> xs = [f x | f <- fs, x <- xs]

http://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-739

Maybeのときと同じく<*>は連続して使うことができます。

> [(^),(+)] <*> [2,10] <*> [1..5]
[2,4,8,16,32,10,100,1000,10000,100000,3,4,5,6,7,11,12,13,14,15]

おっと、さっきの組み合わせよりも大きなリストができてしまいました。
左から順を追って見てみましょう。まずは2*2=4通りの組み合わせリストが返ります。

> let a = [(^),(+)] <*> [2,10]
> :t a
a :: Integral b => [b -> b]
-- [(2^),(10^),(2+),(10+)]

そしてこの関数リストに[1..5]を適用します。組み合わせは4*5=20通りです。

> a <*> [1..5]
> :t a <*> [1..5]
a <*> [1..5] :: Integral b => [b]
-- [2,4,8,16,32,10,100,1000,10000,100000,3,4,5,6,7,11,12,13,14,15]

参考文献

Functorを理解する

この記事ではFunctorというHaskellには欠かせない型クラスについて、概念や捉え方、どういった型がFunctorという振る舞いをできるのかを解説します。

型クラスとは

型クラスとは、値の等値性を判定したり、ある値を文字列として表現したりと特定の振る舞いを定義するインターフェイスです。その特定の振る舞いを定義した関数のことをメソッド、そのメソッドを実装した型をその型クラスのインスタンスと呼びます。

状態という箱

値を関数に適用したいときは次のように書けます。

> (+100) 5
105

Haskellではある状態に値が入っていることがよくあります。例えばMaybe型は値がJust 5に入っていたりするように。このある状態は値を入れる「箱」と考えることができます。

ここでMaybe型に入った値を関数に適用したいとします。しかし、次のように書いてもうまくいきません。

> (+100) (Just 5)
<interactive>:5:1:
    Non type-variable argument in the constraint: Num (Maybe a)
    ...

これはMaybeという箱に値が入ったままで、適用したい関数がその値の取り出し方を知らないからです。そこで活躍するのがFunctorという型クラスとfmapという関数です。

Functorとは

Functorは型クラスです。

早速ですがFunctorの型クラスの定義を見てみましょう。Data.Functorに定義されています。

class Functor f where
  fmap :: (a -> b) -> f a -> f b

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#Functor

fmapという関数が定義されており、この関数は「aを取りbを返す関数」とf aを取り、f bを返します。ここでいうfはファンクター値と呼ばれFunctorのインスタンスとなる型が入ります。

そして、MaybeはFunctorのインスタンスなので当然fmapを実装しています。このときのfmapの型シグネチャは次のようになります。

fmap :: (a -> b) -> Maybe a -> Maybe b

ちょうどファンクター値fがインスタンスであるMaybeに置き換わった感じですね。 ついでにインスタンス定義も見てみましょう。

instance Functor Maybe where
  fmap f Nothing = Nothing
  fmap f (Just a) = Just (f a)

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-652

fmapにはパターンマッチが使われていて箱に入った値がJustかNothingで挙動が違います。Justの場合は箱から値を取り出して関数fに適用してから再度Justに入れ直して返します。一方でNothingの場合は箱には何もせずにそのままNothingのみを返します。

それでは実際に使ってみましょう。

> fmap (+100) (Just 5)
Just 105
> fmap (+100) Nothing
Nothing

うまくいきました。Nothingの場合は箱に値も何も入っていないことと同じなので、何もないものに関数を適用しても何も変化がないのは当然と言えるでしょう。


fmapは中置演算子( <$> )としても提供されています。

> (+100) <$> (Just 5)
Just 105

Functorのインスタンスたち

Maybe以外にもFunctorのインスタンスになれる型があります。

リスト

リストもFunctorです。リストという箱には複数の値が入りますが、リストのfmapは箱から値を1つずつ取り出して関数に適用します。

> fmap (+100) [1..5]
[101,102,103,104,105]

この動作はmap関数と同じものなので、インスタンス定義も次のようになっています。

instance Functor [] where
  fmap = map

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-734

Either

EitherはMaybeと似ていますが、MaybeのNothingと違い失敗を表したいときにも値を付けられます。

data Either a b = Left a | Right b

Maybeファンクターと同じく箱に入った値には何もせずそのまま返します。

> fmap (+100) (Right 5)
Right 105
> fmap (+100) (Left "error...")
Left "error..."

Eitherは型変数を2つ取るのでインスタンス定義が他のものと少し違います。Functorは型クラス定義から分かるように1つの型変数しか取らないので、Eitherのような型変数を複数持つ型をインスタンスにしたい場合はFunctor (Either a)のように部分適用により型変数の数を調整する必要があります。

instance Functor (Either a) where
  fmap _ (Left x) = Left x
  fmap f (Right y) = Right (f y)

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-127

この定義が正しいことはfmapの型シグネチャにファンクター値を実際に代入してみることで確かめることができます。

元のfmapの型シグネチャはこうでした。

fmap :: (a -> b) -> f a -> f b

そしてEitherファンクターのファンクター値はEither aなので、fmapの型シグネチャは次のようになります。

fmap :: (b -> c) -> Either a b -> Either a c

仮にファンクター値がEither a bだった場合は次のようになりますが、Either a b cは最早Either型でないことが分かります。

fmap :: (c -> d) -> Either a b c -> Either a b d

関数

今まで我々が何気なく使っていた関数もまたFunctorです。実際に試してみましょう。

> let a = fmap (*2) (+100)
> :t a
a :: Num b => b -> b
> a 5
210

値として渡した関数に関数を適用しようとすると、どうやら新しい関数を返すようです。これは「箱から値を取り出して関数に適用し、その値をまた箱に入れ直す」というFunctorの性質と合っているように見えます。

新しく返ってきた関数は引数を1つ取り、(+100),(*2)の順で適用して値を返します。この処理は合成関数(.)でやっていることと同じです。

instance Functor ((->) r) where
  fmap = (.)

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-638


この他にもいろんな型がありますが、ファンクターとは別の概念の説明が必要になるのでまたの機会に紹介します。

参考文献

Stateモナドを理解する

ということがあったので主に備忘録としてまとめました。

この記事では 『すごいHaskellたのしく学ぼう!』 (通称「すごいH本」)からコイントスをシミュレーションする関数を書く問題を参考に、Stateモナドについて学んでいこうと思います。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

はじめに

下記のような3回コインを投げてその裏表を返す関数があったとします。

threeCoins :: StdGen -> (Bool, Bool, Bool)
threeCoins gen = do
  let (c1, g1) = random gen
      (c2, g2) = random g1
      (c3, g3) = random g2
  in (c1, c2, c3)

ところがStateモナドを使うことで乱数の状態をより簡単に扱うことができるようになります。

randomSt :: (RandomGen g, Random a) => State g a
randomSt = state random

threeCoins' :: State StdGen (Bool, Bool, Bool)
threeCoins' = do
  c1 <- randomSt
  c2 <- randomSt
  c3 <- randomSt
  return (c1, c2, c3)

Stateモナドとは一体どのようなものなのでしょうか。またそれは通常の関数による値の受け渡しとどう違い、どのように便利なのか。それを知るためにはまず状態を伴う関数について考える必要があります。

状態を伴う関数

System.Randomのrandom関数は、乱数ジェネレータを引数に取り、乱数と新しいジェネレータを返します。random関数が乱数ジェネレータを受け取って新しいジェネレータを返しているのは、ジェネレータの状態を上書きすることができないからです。これは純粋関数型言語の参照透過性によるものです。

random :: (RandomGen g, Random a) => g -> (a, g)

そんなrandom関数の型から、 状態付き計算 という汎用的な型を得ることができます。 状態付き計算は、状態をsとし、計算結果aと更新された状態を返す関数として下記のように表現できます。

s -> (a, s)

Stateモナド

状態付き計算をモナドとして扱えるのがStateモナドです。 Stateモナドは次のような定義となっています。

type State s a = s -> (a, s)

Stateモナドは値コンストラクタ( State )を提供していないので、Stateモナドで状態付き計算を包みたい場合はstate関数を使う必要があります。

Stateモナドの関数たち

提供されている関数は以下のようなものがあります。

  • state
  • runState
  • evalState
  • execState
  • get
  • put

state関数は状態付き計算を受け取ってStateモナドで包んで返します。

> :t state
state :: (s -> (a, s)) -> State s a
> :t state $ \x -> (25, x)
state $ \x -> (25, x) :: (Num a) => State s a

runState関数はStateモナドと状態を受け取って、Stateモナドを解いて状態付き計算を返します。state関数の逆ですね。

> :t runState
runState :: State s a -> s -> (a, s)
> runState (state $ \x -> (25, x)) "neko"
(25,"neko")

evalState・execState関数は計算結果か状態のどちらかだけを返したいときに使います。

> :t evalState
evalState :: State s a -> s -> a
> :t execState
execState :: State s a -> s -> s

get・put関数は、コイントス問題とは直接関係ないので後で説明します。

returnと>>=

returnと>>=の実装があるStateモナドインスタンス定義は次のようになっています。

instance Monad (State s) where
  return x = State $ \s -> (x, s)
  (State h) >>= f = State $ \s -> let (a, newState) = h s
                                      (State g) = f a
                                  in g newState

returnは値を受け取ってそれが結果になるような状態付き計算を返すだけです。

> :t (return "neko") :: State s String
(return "neko") :: State s String :: State s String

Stateモナドを>>=で関数を適用した場合はその結果もまたStateモナドである必要があるため、右辺の始まりは State $ \s -> ... となっています。 まずは 状態付き計算 である左辺のhと、右辺のラムダ式で引数として渡されている 状態 sを使って計算結果aと新しい状態newStateのペアを返しています。 次に左辺の関数fを計算結果aに適用すると新しい 状態付き計算 gを返します。 あとは 状態付き計算 gにnewStateを適用すれば最終的な計算結果と状態のペアを得ることができます。

これでコイントス問題をStateモナドで解くのに必要なことは説明したのでもう一度考えてみましょう。

Stateモナドを使ってコイントス問題を解く

コイントス問題に使われるrandom関数は乱数ジェネレータを受け取って乱数と新しいジェネレータを返すのでした。これをStateモナドとして扱うためにはstate関数を使って包めばよさそうです。

randomSt :: (RandomGen g, Random a) => State g a
randomSt = state random

あとはrandomSt関数を呼んでいって結果を受け取り最後にreturnで返すだけです。

threeCoins' :: State StdGen (Bool, Bool, Bool)
threeCoins' = do
  c1 <- randomSt
  c2 <- randomSt
  c3 <- randomSt
  return (c1, c2, c3)

なぜrandomSt関数に状態付き計算を明示的に渡す必要がないのかは、ラムダ式を使って書いてみると理解できると思います。

threeCoins' = randomSt >>= (\c1 -> randomSt c1 >>= (\c2 -> randomSt c2 >>= return (c1, c2, c3)))

runState関数などに適用して実際に使ってみましょう。

> runState threeCoins' $ mkStdGen 7
((True,False,True),33684305 2103410263)

本編はこれで終わりです。あとは上で説明しなかったStateモナドの関数の説明だけします。

get・put関数

コイントス問題では使いませんでしたが、get・put関数も重要な関数です。

get関数は現在の状態をそのまま計算結果としたStateモナドを返します。

> runState get [1..5]
([1,2,3,4,5],[1,2,3,4,5])

put関数は状態を受け取って、その値で状態を更新したStateモナドを返します。

> runState (do x <- get; put $ (*2) <$> x) [1..5]
((),[2,4,6,8,10])

まとめ

StateモナドはMaybeやリストモナドと比べると特殊なモナドに見えるので敬遠していましたが、Stateモナド無しで書いた関数をStateモナドを使って書き直してみるとその良さが分かるかと思います。自分は特にdo記法によるStateモナドの受け渡しをちゃんと理解していなくて、今回>>=で置き換えることをやったおかげでようやく理解ができた感じでした。

あとはStateモナドの定義が実は type State s a = s -> (a, s) じゃなくて StateT s Identity だとか、state関数はMonadState型クラスの制約があるだとか色々と省いている部分がありますがそれはまた別の機会にまとめます。今回は分かりやすさを重視しました。ソースコードなど詳しくは Control.Monad.State.Strict あたりに書いてあります。