【Rust】高階関数を作るときのメモ その2

前回、簡単な高階関数hogeを書きましたが、もう少しだけ分かったことがあるのでまたメモとして残しておきます。

kudohamu.hatenablog.com


今回は高階関数というよりはRustのクロージャについてです。

※ バーション1.3.0です。Rustは結構頻繁に破壊的な変更が起きるようなので将来ここに書いてある内容は通らない可能性あり。


前回できたもの

前回は以下のように「"ほげー!"という文字列を返す関数」を返す関数hogeを作成しました。

fn main() {
    println!("{}", hoge()());
}

fn hoge() -> Box<Fn() -> String> {
    Box::new(|| -> String {
        String::from("ほげー!")
    })
}

ただ、高階関数を作る時は大抵返り値となる関数の中でゴニョゴニョしたいものです。

返り値の関数に引数を付ける

ということで、hogeを改変して「"ほげー"という文字に引数の文字列をsuffixとして付けた文字を返す関数」を返す関数hogeを作ってみます。

fn main() {
    println!("{}", hoge()("?"));
}

fn hoge() -> Box<Fn(&str) -> String> {
    Box::new(|suffix: &str| -> String {
        format!("ほげー{}", suffix)
    })
}

結果

ほげー?

想定通りの動きをしました。

高階関数hoge)に引数を付ける

ではhogeにも引数prefixを作り、"{prefix}、ほげー{suffix}"となるような関数を返す関数にしてみます。

fn main() {
    println!("{}", hoge(String::from("ほ"))("?"));
}

fn hoge(prefix: String) -> Box<Fn(&str) -> String> {
    Box::new(|suffix: &str| -> String {
        format!("{}、ほげー{}", prefix, suffix)
    })
}

普段通り書いてみましたが、以下のように怒られました。

prog.rs:6:14: 8:6 error: closure may outlive the current function, but it borrows `prefix`, which is owned by the current function [E0373]
prog.rs:6     Box::new(|suffix: &str| -> String {
prog.rs:7         format!("{}、ほげー{}", prefix, suffix)
prog.rs:8     })
prog.rs:7:29: 7:35 note: `prefix` is borrowed here
prog.rs:7         format!("{}、ほげー{}", prefix, suffix)
                                          ^~~~~~
note: in expansion of format_args!
<std macros>:2:26: 2:57 note: expansion site
<std macros>:1:1: 2:61 note: in expansion of format!
prog.rs:7:9: 7:44 note: expansion site
prog.rs:6:14: 8:6 help: to force the closure to take ownership of `prefix` (and any other referenced variables), use the `move` keyword, as shown:
prog.rs:      Box::new(move |suffix: &str| -> String {
prog.rs:          format!("{}、ほげー{}", prefix, suffix)
prog.rs:      })
error: aborting due to previous error

大事そうなメッセージはこの2つ

closure may outlive the current function, but it borrows `prefix`, which is owned by the current function

prog.rs:6:14: 8:6 help: to force the closure to take ownership of `prefix` (and any other referenced variables), use the `move` keyword, as shown:
prog.rs:      Box::new(move |suffix: &str| -> String {
prog.rs:          format!("{}、ほげー{}", prefix, suffix)
prog.rs:      })

クロージャはcurrent function(hoge)の外でも生きるけど、prefixの所有権はcurrent functionにあるよというのと
move キーワードを付ければ?というもの。

move キーワードを付ける

メッセージ通りつけてみます。

fn main() {
    println!("{}", hoge(String::from("ほ"))("?"));
}

fn hoge(prefix: String) -> Box<Fn(&str) -> String> {
    Box::new(move |suffix: &str| -> String {
        format!("{}、ほげー{}", prefix, suffix)
    })
}

結果

ほ、ほげー?

解決した。

引数として文字列を渡しやすいようにStringを&strにリファクタしておく

fn main() {
    println!("{}", hoge("ほ")("?"));
}

fn hoge(prefix: &str) -> Box<Fn(&str) -> String> {
    let string_prefix = prefix.to_string();
    Box::new(move |suffix: &str| -> String {
        format!("{}、ほげー{}", string_prefix, suffix)
    })
}

prefixをto_stringするのがあれだけど呼ぶ側が綺麗になるので良しとしておく。

moveキーワードってなによ

Rustのクロージャはエンクロージャの環境(の変数)を借用するらしい。

少し見てみます。

fn main() {
    let mut out_var = String::from("あー");
    
    let hoge = || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", hoge()); // あー、ほげー
    println!("{}", out_var); // あー

    // mutableである必要がないという警告が出ますが、これ以降のためにmutableしておきます
}
fn main() {
    let mut out_var = String::from("あー");
    
    let hoge = || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", hoge()); // あー、ほげー
    out_var.push_str("いー"); // エラー: out_varはまだhogeが借りている状態なので変更できない
    println!("{}", out_var);
}
fn main() {
    let mut out_var = String::from("あー");
    
    {
        let hoge = || -> String {
            format!("{}、ほげー", out_var)
        };
        println!("{}", hoge()); // あー、ほげー
    } // ここでhogeのlifetimeが終わるのでout_varの所有権が返される
    
    out_var.push_str("いー");
    println!("{}", out_var); // あーいー
}

確かに、Rustのクロージャはエンクロージャの環境(の変数)を借用しています。

振り返る

これを踏まえるとさっきのエラーメッセージ

closure may outlive the current function, but it borrows `prefix`, which is owned by the current function

の意味が分かってきます。
moveキーワードを付ける前の状態ではクロージャはエンクロージャであるhogeの環境にある変数prefixを借りていますが、クロージャがprefixを使い終わって返す前にhogeのlifetimeが終わってしまいますね。
prefixの所有権はhogeにあるのでメモリの開放もhogeの寿命が尽きるのと同時に行われますがその後にクロージャにprefixを参照されるとマズいのでこの動作はもっともです。

でも、prefixを使いたい。

ここでコンパイラに教えてもらったmoveキーワードです。
これのキーワードをクロージャにつけると、エンクロージャの環境(の変数)を借用するのではなく所有権を移動するように変更されるとのこと。

つまりmoveキーワードを付けることでprefixの所有権がhogeからクロージャに移る。よってprefixのメモリの開放はhogeの寿命が尽きるときには行われず、クロージャの寿命が尽きるときに一緒に開放されるようになるということですね。

少し見てみましょう。

fn main() {
    let mut out_var = String::from("あー");
    
    let hoge = move || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", hoge()); // あー、ほげー
}
fn main() {
    let mut out_var = String::from("あー");
    
    println!("{}", out_var); // あー
    
    let hoge = move || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", hoge()); // あー、ほげー
}
fn main() {
    let mut out_var = String::from("あー"); 
    
    let hoge = move || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", out_var); // エラー:out_varの所有権がhogeに移ったので使えない
    println!("{}", hoge());
    println!("{}", out_var); // 同様にエラー
}
fn main() {
    let mut out_var = String::from("あー");
    
    let cloned_var = out_var.clone();
    
    let hoge = move || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", cloned_var); // あー(クローンした変数の所有権は持っているので使える)
    println!("{}", hoge()); // あー、ほげー
    println!("{}", cloned_var); // あー(out_varが解放されても当然使える)
}
fn main() {
    let mut out_var = String::from("あー");
    
    let cloned_var = out_var.clone();
    
    let hoge = move || -> String {
        format!("{}、ほげー", cloned_var) //クローンした変数の方を使う(hogeはcloned_varの所有権をもらう)
    };
    
    println!("{}", out_var); // あー(out_varの所有権は依然として持っているので使える)
    println!("{}", hoge()); // あー、ほげー
    println!("{}", out_var); // あー
}

所有権が移動されています。

所感的な

少しずつ、少しずつだけどRustのメモリ周りについて分かってきた。
この程度の内容はmallocやfreeを書き慣れている人達は息を吸うように分かっているだろうなぁ...。


ただこう、なんというかクロージャは名前の通りエンクロージャの環境を閉包するものだと思っていたのでRustのクロージャクロージャと呼んでいいのかというモヤモヤがあります。
自由変数を実行時の環境ではなく定義したときの環境で使うもので、環境(よく意味論(よく分かってない)とかEnvで表されてるやつ)をまるっとコピーしてもっておくようなイメージでした。
定義とかいう言葉を持ち出すのは怖いのであまりやらないのですが、これも従来のクロージャなのでしょうか?


いつも通り、「ここが間違っている」「こうした方が良い」、「こういうやり方もある」等ありましたらご教示ください。

参考

qiita.com

【Rust】高階関数を作るときのメモ

最近Rustを触っています。
GCに頼りきって生きてきた身なので、普段通りのコードを書いていたところ高階関数を実装しようとしたところで見事にエラーに遭遇しました。
未来の自分と、同じようなエラーに出くわした方のためにメモとして残しておこうと思います。

※ バーション1.3.0です。Rustは結構頻繁に破壊的な変更が起きるようなので将来ここに書いてある内容は通らない可能性あり。


簡単のために「"ほげー!"という文字列を返す関数」を返す関数hogeを実装するという目的で話していきます。

直感的な実装

まずHaskellなんかで書くような気持ちで書いたのが以下(気持ちは分かってもらえるはず...)

fn main() {
    println!("{}", hoge()());
}

fn hoge() -> (|| -> String) {
    || -> String {
        String::from("ほげー")
    }
}

以下のように怒られました。

prog.rs:5:16: 5:18 error: expected type, found `||`
prog.rs:5 fn hoge() -> (|| -> String) {
                         ^~

このあたりで上記のような書き方を見たのでこれでいけるものと思っていましたが、バーションアップの過程でダメになったようです。

返り値の型を変える

返り値の(|| -> String)は型としては認識されていないようなので、関数呼び出し形式で呼び出せる型を表すFnに書き換えたものが以下

fn main() {
    print!("{}", hoge()());
}

fn hoge() -> (Fn() -> String) {
    || -> String {
        String::from("ほげー!")
    }
}

これは以下のようになにやら長文で怒られました。

prog.rs:5:14: 5:30 error: the trait `core::marker::Sized` is not implemented for the type `core::ops::Fn() -> collections::string::String` [E0277]
prog.rs:5 fn hoge() -> (Fn() -> String) {
                       ^~~~~~~~~~~~~~~~
prog.rs:5:14: 5:30 note: `core::ops::Fn() -> collections::string::String` does not have a constant size known at compile-time
prog.rs:5 fn hoge() -> (Fn() -> String) {
                       ^~~~~~~~~~~~~~~~
prog.rs:6:5: 8:6 error: mismatched types:
 expected `core::ops::Fn() -> collections::string::String`,
    found `[closure prog.rs:6:5: 8:6]`
(expected trait core::ops::Fn,
    found closure) [E0308]
prog.rs:6     || -> String {
prog.rs:7         String::from("ほげー!")
prog.rs:8     }
error: aborting due to 2 previous errors

大事そうなのはこの部分

> the trait `core::marker::Sized` is not implemented for the type `core::ops::Fn() -> collections::string::String

>`core::ops::Fn() -> collections::string::String` does not have a constant size known at compile-time


Sizedというトレイトを実装した型ではないということと、コンパイル時に固定サイズを決められない(持ってない)というメッセージです。
どちらもコンパイル時に必要なサイズが分からないよということですね。
一応(関数)型としては認識されてそうです。

スタックとヒープ

確認として一応...

  • スタック(領域):コンパイラやOSが割り当てるメモリ領域
  • ヒープ(領域):アプリケーションやプログラム実行中に動的に確保するメモリ領域


関数の引数や返り値はスタックに積むのでコンパイル時に各変数などに対してそれぞれどれだけのメモリサイズを確保していればよいかを分からなければなりません。
i32とi64では割り当てられるサイズが違います。
Sizedというトレイトはi32のように型だけではサイズが分からない型が、「どれだけのメモリサイズを確保していればよいか分かっているよ」ということを示すトレイトらしい。


上のエラーメッセージを再度確認します。
() -> Stringな関数型はまず型からどれだけのメモリサイズを確保していればよいか分からない。またSizedトレイトも実装されていないのでスタックに詰めない。
なるほど。コンパイラの気持ち、分かってきた(嘘)


スタックがダメならヒープに積め(置け)ばいいじゃない

じゃあこの返り値である関数をヒープに置けばよいのではないか。
ここで使えるのがBoxです。

ヒープに置いていることを示すポインタ型のようです。
Boxを使うとこのようになる。

fn main() {
    println!("{}", hoge()());
}

fn hoge() -> Box<Fn() -> String> {
    Box::new(|| -> String {
        String::from("ほげー!")
    })
}

これをrust runすると

ほげー!

いけました。

所感的な

これで簡単な高階関数はできましたが、そんなカジュアルにヒープに置いていいのか?Box使っていいのか?ということが気になりました。
GCに頼りきってきたせいでこのあたりの塩梅がよく分かっていない、自信がないのは本当にダメなとこですね...。

僕の中で高階関数を使うといえばパーサーコンビネータが一番に出てくるので、Rustで有名なパーサーコンビネータを調べたところcombineというのが出て、その中でもちょろちょろ使われているよう。github.com



Rustは本当に色んな機能があるので、多分これ以外にも実装方法があると思います。
「これは良くない」「こっちの方がいい」とか「こういうやり方もある」なんかがあったら是非教えてください。(切実です)


続き(引数を渡したりする)kudohamu.hatenablog.com


【AWS】ansibleでEC2インスタンスにプロビジョニングする【ansible】

ansibleを使ってec2にプロビジョニングするときの導入部分をメモ程度に。

ansible自体のインストールは済ませてあるものとします。

 

まずhostsファイルを作ります。

 ~/ansible/hosts  <- フォルダは好きなところで構いません
[kudohamu-ec2s]
xx.xxx.xx.xx <- ec2インスタンスのIP

 

次にplay-bookの作成。今後ec2を増やすことを考慮してここではcommon.ymlという名前にします。

common.ymlでやること
  • ユーザ(kudohamu)の作成
  • kudohamuユーザにsudo権限を付与
  • gitのインストール

 

サーバではなるべくrootユーザを使わないようにします。必要なユーザを作って必要な権限を与え、そのユーザで作業するのが一般的です。

とはいえ趣味のEC2なのでひとまずkudohamuというユーザを作りsudo権限を与えます。権限部分は必要なものを設定してください。

ついでにgitだけインストールしてみることにします。

~/ansible/common.yml
- hosts: kudohamu-ec2s
sudo: yes
vars:
password: $1$kudohamu$xD7dcxxxxxxxxxx/.
tasks:
- user: name=kudohamu password={{password}} shell=/bin/bash state=present append=yes
- name: add a sudo to kudohamu
lineinfile: "dest=/etc/sudoers backup=yes state=present regexp='^kudohamu' line='kudohamu ALL=(ALL) ALL'"
- yum: name=git state=latest

ec2のOSはRHEL6.5を使っていますがAmazonLinuxでもこのままで大丈夫なはずです。Ubuntuとか使っている人はyumコマンドをそれぞれのパッケージ管理システムのものに変えてください。 

 

ユーザのパスワードは生のものを直書きしないようにします。

このファイルを見られてしまったら簡単に乗っ取られてしまいますからね!

下のopensslコマンドを打つことで暗号化したパスワードが出力されます。

$~/ansible% openssl passwd -salt <ソルト文字列> -1 <パスワード>
$1$kudohamu$xD7dcxxxxxxxxxx/.
$~/ansible%

この値をcommon.ymlにコピペしましょう。

ソルト文字列とパスワードはお好きな値を入れてください。

 

ここまででcheckオプションを付けて実際にプロビジョニングできるかテストしてみます。

$~/ansible% ansible-playbook -i hosts common.yml --check
PLAY [kudohamu-ec2s] **********************************************************

GATHERING FACTS ***************************************************************
fatal: [xx.xxx.xx.xx] => SSH encountered an unknown error during the connection. We recommend you re-run the command using -vvvv, which will enable SSH debugging output to help diagnose the issue

TASK: [user name=kudohamu password=$1$kudohamu$xD7dcxxxxxxxxxx/. shell=/bin/bash state=present append=yes] ***
FATAL: no hosts matched or all hosts have already failed -- aborting


PLAY RECAP ********************************************************************
to retry, use: --limit @/Users/kudohamu/common.retry

xx.xxx.xx.xx : ok=0 changed=0 unreachable=1 failed=0

む。

-vvvvオプションを付けて具体的なエラー内容を見てみたところログインに失敗しているようです。

そういえばpem使っていません。

 

hostsファイルを下記のように書き換えます。

[kudohamu-ec2s]
xx.xxx.xx.xx ansible_ssh_user=ec2-user ansible_ssh_private_key_file=~/.ssh/kudohamu-ec2-test-key.pem

初期状態のawsにログイン出来るユーザはec2-userなのでansible_ssh_userにそれを指定します。

またec2インスタンスを立ち上げる時に作成(もしくは指定)したプライベートキー(.pem)の場所をansible_ssh_private_key_fileに指定します。

 

これでもう一度チェック

$~/ansible% ansible-playbook -i hosts common.yml --check

PLAY [kudohamu-ec2s] **********************************************************

GATHERING FACTS ***************************************************************
ok: [xx.xxx.xx.xx]

TASK: [user name=kudohamu password=$1$kudohamu$xD7dcxxxxxxxxxx/. shell=/bin/bash state=present append=yes] ***
changed: [xx.xxx.xx.xx]

TASK: [add a sudo user] *******************************************************
changed: [xx.xxx.xx.xx]

TASK: [yum name=git state=latest] *********************************************
changed: [xx.xxx.xx.xx]

PLAY RECAP ********************************************************************
xx.xxx.xx.xx : ok=4 changed=3 unreachable=0 failed=0

いいですね。

 

checkオプションを外して実際に入れてみましょう(エラーの内容によってはcheckでは通っても実際には通らないこともあります)。

$~/ansible% ansible-playbook -i hosts common.yml

PLAY [kudohamu-ec2s] **********************************************************

GATHERING FACTS ***************************************************************
ok: [xx.xxx.xx.xx]

TASK: [user name=kudohamu password=$1$kudohamu$xD7dcxxxxxxxxxx/. shell=/bin/bash state=present append=yes] ***
changed: [xx.xxx.xx.xx]

TASK: [add a sudo user] *******************************************************
changed: [xx.xxx.xx.xx]

TASK: [yum name=git state=latest] *********************************************
changed: [xx.xxx.xx.xx]

PLAY RECAP ********************************************************************
xx.xxx.xx.xx : ok=4 changed=3 unreachable=0 failed=0

ちゃんと入りました。

 

あとはいつも通りplay-bookにプロビジョニングしたいものを書いていくだけです。

ansibleでのプロビジョニングの詳しい書き方やファイルの構成等は僕もまだまだ勉強中なので他のサイトなり本なりでお願いします。

 

小・中規模のサーバを整えるにはansibleが手軽すぎますねー。

 

 

 

( 'ω' )

【Golang】第5回 dwanGoに参加しました

タイトル通りですが、先週の水曜(2014/12/10)にドワンゴさんの社員の方が開かれているdwanGoというGo言語の勉強会に参加してきました。


dwanGo - connpass

 

 

内容 

今回のテーマは「コマンドラインツールを書いてみましょう」というものでした。私はこの第5回が初参加でしたが、これまではA Tour of Goもくもく会のようでしたので参加タイミングとしてはちょうど良かったかも。

 

会自体は、最近ドワンゴさんに入社されたという@yukkuri_shinaiさんによって進められました。

コマンドラインツールは@yukkuri_shinaiさん曰く「劣化aria2」のような並列ダウンローダを作ろうということでした。

制作は

  1. httpで指定のファイルを落としてくるツールを作る
  2. ファイルの指定を引数で、かつ複数個指定できるように改良する
  3. 複数のファイルを並列で落とすように改良する
  4. フレームワークを使ってヘルプやバージョンが出せるように改良する

という4段階に分かれており、各段階ごとにgistにupされた答え(兼写経用)を見ながら@yukkuri_shinaiさんによる解説が行われました。

 

次の段階になるまでどう改良されていくのか分からなかったのと、各段階での要件がおおまかなものだったのもあって、次の段階のものを先に作ってしまっていたりもしました。

 

 

書いたコード

 

 

結果

dwgo05 http://otiai10.com/chino.gif http://otiai10.com/enju.gif

このように実行すると以下のような画像がダウンロードされます。

 

f:id:kudohamu:20141215131459g:plain

f:id:kudohamu:20141215131513g:plain

URLは適用にぐーぐる先生で探したものです。

まだまだ勉強不足で、syncパッケージの知識が怪しかったので一部写経しています。 

 

 

参加してみて

codegangsta/cliというコマンドラインツールを作るフレームワークの存在を知ることができたのは大きな収穫でした。

これまではgo-flagsを使っていましたが、今回紹介されたcodegangsta/cliの方が私の好みに合っていました。これからはこちらに乗り換えようかなと思います。


jessevdk/go-flags · GitHub

 


codegangsta/cli · GitHub

 

@yukkuri_shinaiさんが用意した写経用のコードが正しく動かないものだったり(修正済み)クリックが右クリックになったままになり再起動するなどのちょっとしたハプニングはありましたが、そのおかげで逆にほのぼのした会になっていたように思います。

内容次第ですが第6回も参加したいです。

 

それにしてもみんな書くの早いし、質問の内容も鋭いとこ突いてたりですごいなぁ

 

 

おまけ

Goのファイルアイコン可愛い

f:id:kudohamu:20141215131913p:plain

 ʕ◔ϖ◔ʔ

【Golang】Go言語からPostgreSQLを使う【sql, lib/pq】

前回のプログラムを改良してPostgreSQLを使ってメッセージを返すように改良してみました。


【Golang】公開鍵と共通鍵で暗号化する【rsa, aes, pem, x509, cipher】 - くどはむと猫の窓

 

今回は以下を参考にしました。

 

ほとんどここに書いてあるサンプルの通りです。

まずはdatabase/sqlと使用するデータベースのDriverパッケージをインポートする必要があります。今回はPostgreSQLを使用するのでgithub.com/lib/pqパッケージを使います。

その他のDriverパッケージについてはここに一覧があるので参考にしてください。

SQLDrivers - go-wiki - SQL database drivers - Go Language Community Wiki - Google Project Hosting

インポートはこのような感じです。

import (
	_ "github.com/lib/pq"
	"database/sql"
)

この_(アンダースコア)はブランク修飾子呼ばれるものです。

/lib/pqパッケージは直接は使われません。sql/driverパッケージからインターフェースを通して呼び出されます。

しかしGo言語では直接呼びだしていないパッケージがあるとコンパイル時に怒られます。そこでブランク修飾子を使用し、参照だけしてパッケージの中身を直接使わないことを明示的に宣言することでコンパイルエラーを回避できるのです。

DBとの接続は

db, err := sql.Open("postgres", "user=username dbname=hogedb password=huga sslmode=disable")
checkError(err)
defer db.Close()

 こんな感じです。接続の仕方はDBの種類によって多少変わるようです。

SSLモードは以下の4つがあり、今回はDBとの接続にSSLは使っていません。

disable SSLを使用しない
require SSLを使用するが証明書の検証はしない
varify-ca SSLを使用し、信頼できるCAによって署名されたものかを検証する
varify-full SSLを使用し、信頼できるCAによって署名されたものか、サーバーのホスト名が証明書に書かれているものと一致するかを検証する

 

返り値のDB型を使ってSQLの発行などができます。

参照系は

rows, err := db.Query("SELECT * FROM hoge WHERE huga = $1;", huga)
checkError(err)

column := 0
for rows.Next() {
  err = rows.Scan(&column)
  checkError(err)
...処理...
}

のように複数件のレコードを取得するQueryと

cnt := 0
err = db.QueryRow("SELECT COUNT(*) AS cnt FROM hoge WHERE huga = $1;", huga).Scan(&cnt)
checkError(err)

のように一件取得するQueryRowがあります。

更新系は

_, err = db.Exec("INSERT INTO hoge_hugas(hoge, huga) VALUES($1, $2);", hogee, hugaa)
checkError(err)

のようにExec関数を使います。

ここらへんの使い方はあまり他の言語と変わりませんね。

 

と、これらを使ってクライアントから送られて来たメッセージをDBに格納しておき、送られて来たメッセージが

  • 初めてのものなら「'msg'...わたし、気になります!」
  • 初めてでないなら「'msg'はもうn回も聞いたのでわたし、気になりません!」

と返すプログラムに改良してみました。

 

実行結果(クライアント側)

f:id:kudohamu:20141129120159j:plain

 

f:id:kudohamu:20141129120303j:plain

 

次は今まで使ったパッケージの総まとめとして少しちゃんとしたプログラムを作ってみようと思います。

それが終わったらWebアプリケーションかなー。ʕ◔ϖ◔ʔ

【Rails】CarrierWaveでアップロードした画像が読み込めなくてハマった話

最近の興味はもっぱらGo言語ですが、半分授業半分趣味という感じでテーブル60~70個ほど、中規模いかないくらいのWebサイトをRailsで作っています。

画像のアップロード処理にCarrierWaveというgemを使っていますがこれを使う過程でハマってしまい、解決にそこそこ時間を費やしたのでメモしておこうと思います。

 

ハマった際の現象

  • 画像のアップロードはできる(指定したフォルダに画像がアップロードされている)
  • @model_name.image.urlなどで画像のフルパスは返ってくる
  • @model_name.image?ではfalseが返ってくる
  • Viewに表示することができない
  • File.open(@model_name.image.url)で開けない

というものです。

 

原因その1:アップロード先のフォルダがpublic配下以外の場所を指定している

考えてみれば当たり前のことでした。アップロード先をS3などapp配下以外にしている場合はパーミッションなどを見なおしてみるとよいかもしれません。

 

 

原因その2:filenameメソッドの中でuuidを生成している

こちらが原因であることを特定するまでに時間を要しました。

例えばrails g uploader hoge_uploaderというコマンドでHogeUploaderクラスを作成すると、filenameメソッドコメントアウトされてついてくると思います。これはアップロードされた画像のファイルを変更する場合に使うメソッドですが、ここで私は以下のようにしていました。

...これがまずかったようです。とは言っても何かエラーが吐かれるわけでもなく、画像名はちゃんとUUIDになっていますし、macのfinderやプレビューでは画像自体は開くことができますがこれではRailsは開けないようです。

正しくはCarrierWaveのwikiにも載っているように

とするべきみたいです。

これで読み込むことができました。

 

ちなみに...

と秒までの日時で命名する処理で作成された画像は読み込むことができますが

 と6桁のミリ秒を追加すると読み込めなくなります。

...謎です。

 

ひとまずなぜこれで動くのかを特定しようとしましたが、いかんせんCarrierWaveがgemである(Railsの標準ではない)こと、Railsが下層の処理の部分を隠し過ぎ引き受けている親切言語であることなどの理由で中々特定できていません。ひとえに僕の力不足です。

時間のあるときにCarrierWaveの開発者にメールで質問してみることにします。

 

【Ruby】invalid byte sequence in US-ASCII (ArgumentError) が出たとき【Rails】

RailsRubyでもですが)を使っていて例えば何かの画像を開こうとFile.openメソッドを使った時に

/usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/notifications.rb:228:in `split': invalid byte sequence in US-ASCII (ArgumentError)
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/notifications.rb:228:in `failure_lines'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/notifications.rb:164:in `colorized_message_lines'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/notifications.rb:191:in `fully_formatted'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/notifications.rb:95:in `block in fully_formatted_failed_examples'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/notifications.rb:94:in `each'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/notifications.rb:94:in `each_with_index'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/notifications.rb:94:in `fully_formatted_failed_examples'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/formatters/base_text_formatter.rb:34:in `dump_failures'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/reporter.rb:134:in `block in notify'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/reporter.rb:133:in `each'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/reporter.rb:133:in `notify'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/reporter.rb:114:in `finish'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/reporter.rb:55:in `report'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/runner.rb:107:in `run_specs'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/runner.rb:85:in `run'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/runner.rb:69:in `run'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/lib/rspec/core/runner.rb:37:in `invoke'
        from /usr/local/rvm/gems/ruby-2.1.2/gems/rspec-core-3.1.4/exe/rspec:4:in `<top (required)>'
        from /usr/local/rvm/gems/ruby-2.1.2/bin/rspec:23:in `load'
        from /usr/local/rvm/gems/ruby-2.1.2/bin/rspec:23:in `<main>'
        from /usr/local/rvm/gems/ruby-2.1.2/bin/ruby_executable_hooks:15:in `eval'
        from /usr/local/rvm/gems/ruby-2.1.2/bin/ruby_executable_hooks:15:in `<main>'

のようなエラーが出た場合の解決法としてFile.openメソッドの第二引数に

"r:UTF-8"

"rb:UTF-8"

 

等を指定するというのは調べるとよく出てきますが、それでもうまくいかない場合は以下の2つも確認してみると良いかもしれません。

  1. そもそもパスが間違っている
  2. macのプレビューの「書き出し」などで拡張子を変換した(jpg -> png)などの画像を開こうとしている

この2つでもinvalid byte sequence in US-ASCII (ArgumentError) が出ます。

1.の場合にはもう少し違うエラーが出て欲しい...。