関係と代数的データ型との相互変換についての妄想 その1

OR マッパーみたいな、関係*1とデータ型との相互変換が機械的にできたら嬉しいなという話。

シンプルなのは簡単だしすでにできる。例えば次のような関係がある場合*2

var user base relation {
  id integer,
  name string,
}
key { id };

var friends base relation {
  user1 integer,
  user2 integer
};

これを単に次のようなデータ型に変換するのはいくらかのライブラリーで実現できる。

data User = User { id: Int, name: String }

data Friends = Friends { user1: Int, user2: Int }

しかし、実際のところ欲しい型というのは次の通りでこれは機械的にできるのはなさそう。(ちゃんとは調べてないのであったら教えてほしい。)

data User = User { id: Int, name: String, friends: [User] }

よくあるクラスベースのオブジェクト指向言語なら Friends ゲッターの中でクエリー発行すればいいんだけど、同じことを Haskell ですると IO まみれになるしできれば純粋な函数にしたいよなー。

public class User {
  public readonly int id;
  public readonly string name;
  readonly IList<User> friends;
  public IList<User> Friends {
    get {
      if (friends == null) friends = …;
      return friends;
    }
  }
  public User query(int id) { … }
}

妄想としては、そのフィールドが使われてるかの情報を型に乗っけることで、DB からの取得時にどれを取ってくればいいか分からないかなーと。

do
  user <- queryUser 1 -- 何らかの queryUser :: Int -> MonadDB User みたいな函数があるとして
  return $ friends user

friends 函数の型を GetFriends user => user -> [User] みたいにしてやることで型推論によって queryUser 函数の型クラス制約にも GetFriends が付いて実装を変えられないかなぁと。

うーんでも型クラスのありなしでは実装は変えられないかー。なんとか型に情報乗せないとダメかな。

続きはまた後日。

*1:ここでいう関係は関係代数の用語としての関係です。

*2:Tutorial D の記法に従ってますが、パーサーにかけたことがないのでまちがってるかもしれない。

技術書典2にサークル参加します 「Haskell Yesod 本」

4月9日日曜日にアキバ・スクエアにて開催される技術書オンリー同人誌即売会技術書典2」にサークル「趣味はデバッグ……」として参加します。

冬コミで頒布した『遠回りして学ぶ Yesod 入門』の誤字脱字等を修正して組版を改善したものを持っていきます。

新作はちょっと間に合うか……

内容

内容は、下記となります。

  • ビルドツール Stack
  • 効率のよい文字列の扱い
  • Haskell の言語拡張
  • コンパイル時計算 Template Haskell
  • Web Application Interface とは
  • 簡単な Yesod の解説
  • ロガー用 Middleware を作る

詳しい内容は見本誌で確認ください。

ロガー用 Middleware の章は @syocy さんに書いていただきました。

紙面版仕様

  • 表紙フルカラー
  • 本文モノクロ
  • B5 判
  • 76ページ(表紙込み)

電子版

BOOTH にて販売中です。誤字脱字等修正済みです。

紙面版購入者は対面電書にて誤字脱字等修正版をダウンロードできます。

価格

価格は下記の通りとなります。

  • 現地販売(現金・クレジットカード)
    • 紙面+電子版
      • 1000円
    • 電子版
      • 800円
      • ダウンロード URI を渡します
  • オンライン販売
    • 電子版
      • 1000円

クレジットカード決済は、Square 社が対応しているものに限ります。

スペース

スペースは「う-04」です。

techbookfest.org

よろしくお願いします!

Yesod で1ページに複数個フォームがある場合は identifyForm を使う

resolver lts-5.4 で確認。

問題

1ページに複数個フォームがある場合、runFormPost はそれぞれのフォームの区別をしてくれません。どういうことかというと、例えば下記のような2つのフォームを利用するとします。

data AFormData = AFormData Text
aForm :: Html -> MForm Handler (FormResult AFormData, Widget)
aForm = renderDivs $ AFormData <$> areq textField "A Text" Nothing

data BFormData = BFormData Text
bForm :: Html -> MForm Handler (FormResult BFormData, Widget)
bForm = renderDivs $ BFormData <$> areq textField "B Text" Nothing

この2つのフォームを1つのページに配置し、ブラウザーでアクセスしどちらか一方のフォームに “Hello” とでも入力し送信します。すると runFormPost aFormrunFormPost bForm も返り値の FormResult Xxx 型の部分が FormSuccess xxx となり、xxx の部分はそれぞれ AFormData "Hello"BFormData "Hello" となります。

これは困ります。

解決

identifyForm を使います。

identifyForm
  :: Monad m
  => Text -- ^ Form identification string.
  -> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))
  -> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))

第1引数にユニークな文字列を指定してください。

aForm = identifyForm "a-form" $ renderDivs $ AFormData <$> areq textField "A Text" Nothing

これで、対応するフォームに入力されたときだけ runFormPostFormSuccess を返すようになります。

参考

Issue yesod-web/yesod #649

Yesod と HDBC-mysql と haskell-relational-record で “Commands out of sync”

Yesod と HDBC-mysqlhaskell-relational-record を一緒に使っているのだが、MySQL サーバーに「Commands out of sync; you can’t run this command now」って言われてぐぬぬってなって、最近デバッグをがんばっている。とりあえず途中経過をメモしておく。

そもそも MySQL プロトコルの呼び出し順とか知らないので苦戦していた。

この辺でエラーが出ている。結合をしなければエラーは出ない。

import Import hiding (on)
import Database.Relational.Query (on)
import qualified Model.Table.Account as Account
import qualified Model.Table.Tweet as Tweet
import qualified Model.Table.User as User

getTweetR :: AccountIdParam -> TweetIdParam -> Handler Html
getTweetR accountIdParam tweetIdParam = do
    user <- requireAuth
    p <- runRelational $ do
             accounts <- runQuery Account.selectAccount accountIdParam
             ts <- flip runQuery () $ relationalQuery $ relation $ do
                       t <- query Tweet.tweet
                       u <- query User.user
                       on $ t ! Tweet.userId' .=. u ! User.id'
                       wheres $ t ! Tweet.id' .=. value tweetIdParam
                       return $ (,) |$| t |*| u
             return (accounts, ts)
    case p of
        ([account@(Account _ _ _ _)], [(tweet@(Tweet _ _ _ _ _), tweetUser@(User _ _ _ _))]) -> do
            form <- generateFormPost commentForm
            defaultLayout $ do
                headerWidget $ Just user
                tweetWidget account user tweet form
        _ -> notFound

とりあえず HDBC-mysqlFFI で libmysqlclient を呼んでるので C のレベルでどの関数を呼んでいるのかログに出してみる。コードはこんな感じ。めっちゃ地道にログを挟んでいった。一部引用すると下記。

mysql_stmt_prepare :: Ptr MYSQL_STMT -> CString -> CInt -> IO CInt
mysql_stmt_prepare mysql a1 a2 = do
  s1 <- peekCString a1
  hPutStrLn stderr $ "mysql_stmt_prepare " ++ (show mysql) ++ " " ++ (show s1) ++ " " ++ (show a2)
  cr <- mysql_stmt_prepare_ mysql a1 a2
  hPutStrLn stderr $ "\t→ " ++ (show cr)
  return cr

そのログ出力が下記(別ページで開く)。同じ接続・同じステートメントに同じ色を塗った。

どうも mysql_stmt_execute した後に mysql_stmt_fetch せずに mysql_stmt_prepare するとダメっぽい。C で確認してみると下記のコードコメントアウトしてるところのありなしでエラーが出たり出なかったりするのでおそらく合ってる。

MYSQL_BIND result;
char display_name[100];
unsigned long display_name_length;
my_bool display_name_error;
memset(&result, 0, 1);
result.buffer = display_name;
result.buffer_length = sizeof(display_name) * sizeof(display_name[0]);
result.is_null = NULL;
result.length = &display_name_length;
result.error = &display_name_error;
{
    my_bool error = mysql_stmt_bind_result(stmt, &result);
    if (error) {
        fprintf(stderr, "mysql_stmt_bind_result\n");
        mysql_close(mysql);
        exit(1);
    }
}
// {
//     int fetch_result = mysql_stmt_fetch(stmt);
//     switch (fetch_result) {
//         case MYSQL_NO_DATA:
//             printf("mysql_stmt_fetch: MYSQL_NO_DATA\n");
//             break;
//         case MYSQL_DATA_TRUNCATED:
//             printf("mysql_stmt_fetch: MYSQL_DATA_TRUNCATED\n");
//             break;
//         default:
//             printf("mysql_stmt_fetch: %d\n", fetch_result);
//     }
//     printf("display_name: %s\n", display_name);
//     switch (result.buffer_type) {
//         case MYSQL_TYPE_BLOB:
//             printf("display_name type: MYSQL_TYPE_BLOB\n");
//         default:
//             printf("display_name type: %d\n", result.buffer_type);
//     }
//     printf("display_name_length: %ld\n", display_name_length);
//     printf("display_name_error: %d\n", display_name_error);
// }
{
    my_ulonglong rows = mysql_stmt_affected_rows(stmt);
    printf("mysql_stmt_affected_rows: %ld\n", (long)rows);
}
{
    MYSQL_STMT *stmt2 = mysql_stmt_init(mysql);
    if (stmt == NULL) {
        fprintf(stderr, "mysql_stmt_init: error\n");
        mysql_close(mysql);
        exit(1);
    }
    {
        const char * const stmt_str = "SELECT `email` FROM `user` WHERE `id` = ?";
        int error = mysql_stmt_prepare(stmt2, stmt_str, strlen(stmt_str));
        if (error) {
            fprintf(stderr, "mysql_stmt_prepare: error\n");
            fprintf(stderr, "error message: %s\n", mysql_stmt_error(stmt2));
            mysql_close(mysql);
            exit(1);
        }
    }
}

この FFI 呼び出しがどういう経緯で呼ばれてるかを探すのはこれから。

接続を使い回すようにしたのがダメなのかもしれない。

解決しました。(2017.03.17)

正格版の runQuery' があったのですね。

『遠回りして学ぶ Yesod 入門』誤字脱字等を修正した電子版を公開しました

先日の投稿で公開した正誤表の内容を反映した電子版を公開しました。

kakkun61.hatenablog.com

コミックマーケット 91 で購入した方は対面電書で、オンラインで購入した方は BOOTH で更新後のファイルを入手することができます。

『遠回りして学ぶ Yesod 入門』正誤表

kakkun61.booth.pm

C91 版

電子版では修正済みです。

ページ 章節 補足
3 はじめに Yesod の初歩的な解説が書かれた本の上巻です。 Yesod の初歩的な解説が書かれた本の上巻です。
7 1 リンク切れの検出や リンク切れの検出
17 3.2 これは quasi quote(準引用)と呼ばれる これは quasi quotes(準引用)と呼ばれる 表記の統一
20 4.1 モリーに文字のデータが モリーに文字のデータが
23 5.1 GHC Users Guidehttp://wiki.haskell.org/GHC GHC Users Guide http://wiki.haskell.org/GHC 空白の追加
45 8.2 stack build yesod-bin stack install yesod-bin
47 8.4 テンプレートの / のページは図 8.1 です。 yesod-mysql テンプレートの / のページのスクリーンショットは図 8.1 です。 分かりにくかったのでより説明的に
50 8.6 yesod-mysql を例に、 yesod-mysql テンプレートを例に 表記の統一
53 9.2 Elm を使えるようにしたライブラリー等もあります。 (どうも Yesod で Elm を使うライブラリーは古く使えなさそうです。)
56 10.2 型制約の ~ は両辺の型が同値であるという制約を表します。 (この文は、次の「新出 API の型」についての解説です。紙面の修正をしたときに入れ違いになってしまいました。)

技術書典 2 版

下記は電子版未修正です。

ページ 章節 補足
21 4.3 「あ」を UTF-16 エンコードすると 3042 「あ」を UTF-32 エンコードすると (00003042)16 2017.07.25 追記。
HaskellCharUnicode コードポイント(UTF-32)を表します。参照

他に見付けた場合は、ここのコメント等でお知らせください。

更新履歴

  • 2017.01.30:C91 版 正誤表公開
  • 2017.07.25:技術書典 2 版 正誤表公開

Adobe Typekit のフォントファイルの場所(Windows)

アプリがフォントをうまく認識しない場合にフォントファイルを直接インストールして対処したりできる。同一マシンならライセンス的にも大丈夫なはず。保証はしません。

環境

場所

%appdata%\Adobe\Adobe Photoshop CC 2017\CT Font Cache にメタファイルがある。↓ のような項目が並んでいる。

%BeginFont
Handler:DirectoryHandler
FontType:Type1
FontName:AdobeDevanagari-Bold
FamilyName:Adobe Devanagari
StyleName:Bold
FullName:Adobe Devanagari Bold
MenuName:Adobe Devanagari
StyleBits:2
FamilyNameNative:Adobe Devanagari
StyleNameNative:Bold
FullNameNative:Adobe Devanagari Bold
WritingScript:Devanagari
OutlineFileName:C:\Program Files\Adobe\Adobe Photoshop CC 2017\Required\typesupport\.f\\.1
DataFormat:sfntData
UsesStandardEncoding:yes
isCFF:yes
hasSVG:no
FileLength:335268
FileModTime:1480434360
WeightClass:700
WidthClass:5
AngleClass:0
DesignSize:110
NameArray:0,Mac,4,Adobe Devanagari Bold
NameArray:0,Win,1,Adobe Devanagari
%EndFont

FullName で目当てのフォントを見つけて、OutlineFileName のファイルに ttf 拡張子を付けると Windows がフォントファイルとして認識してくれるので、インストールするなどする。

%appdata%\Adobe\CoreSync\plugins\livetype\r にもフォントファイルがある模様。

参考

stackoverflow.com

Mac の情報とちょっと古い Windows の情報がある。