20190521の新人プログラマ応援に関する記事は3件です。

[Ruby] 複数の方法がある場合はより特化した方を選ぼう、Ruby を信じて (初心者向け)

はじめに

Ruby は非常に多彩な文法やクラス、メソッドを持っており、1 つの処理を実装するのに複数の方法が存在することが多いです1。そのため、どの方法を採用すればよいか迷ったり、あるいはよりよい方法がないか気になったりする機会も多いです。そこで、そういうケースで役立つささやかな指針を提案します。それが

ある処理を実装するために複数の方法がある場合は、より特化した方を選ぼう

です。

具体例

ケース 1: 整数の配列の合計値を求める

配列などの Enumerable (列挙可能) なクラスを操作する場合、Array#each や Enumerable#map に続いて Enumerable#inject の使い方を学ぶ方も多いと思います。リファレンスには inject メソッドを用いて整数の配列の合計値を求めるコードが記載されています。

[2, 3, 4, 5].inject { |result, item| result + item } #=> 14

Ruby にある程度詳しくなった方であれば Symbol#to_proc の挙動を理解した上で次のように書くかもしれません。

[2, 3, 4, 5].inject(:+) #=> 14

しかし、ここで立ち止まって考えたいのは、より特化した方法があるのではないかと模索することです。Ruby にはあたかもプログラマの思考を先回りしたかのように、おあつらえ向きのメソッドを用意してくれていることがよくあります2

「合計を求めるのだから sum という名前のメソッドはないか」と Array のリファレンスで sum をページ内検索してみましょう。やはりありました :clap_tone1:

[2, 3, 4, 5].sum #=> 14

ケース 2: 配列の先頭から n 個の要素を取得する

次の配列から先頭の要素を 2 つだけ抜き出したいです。

girls = ['ゆの', '宮子', '沙英', 'ヒロ']

先頭の要素を 1 つだけ抜き出すなら

girls[0] #=> "ゆの"

と書いたり、あるいは Array#first を使用して

girls.first #=> "ゆの"

と書けます。わざわざ先頭の要素を取得することに特化したメソッドが first という名前で用意されているのが、まさに Ruby らしいですね!

では先頭から複数の値を取得するためにはどうしたらよいでしょうか。

例えば Ruby と同じスクリプト言語の Python なら、スライスと呼ばれる機能を使用して次のように書けます。

# Python の世界
>>> girls = ['ゆの', '宮子', '沙英', 'ヒロ']
>>> girls[0:2]  # スライスという機能を使って、0 番目 (先頭) から 2 つの要素を取得する。
['ゆの', '宮子']

しかし Ruby の配列にはスライスという機能はなく、同じ書き方をするとシンタックスエラーとなります。

girls[0:2] # SyntaxError

でも Ruby のことなので、必ず Python のスライスと同じような機能が存在するはずです!Ruby を信じるのです :pray:

今度は Array のリファレンスで slice をページ内検索してみましょう。Array#slice というメソッドが見つけるはずです。

girls.slice(0, 2) #=> ["ゆの", "宮子"]

お見事!さすが Ruby :clap_tone1:

しかし喜ぶのはまだ早い!先ほど登場した Array#first のリファレンスをよく読んでみてください。第 1 引数を指定することで少し挙動が変わるのです。

girls.first(2)
#=> ["ゆの", "宮子"]

なんと先頭から 2 つの要素を取得できました。Ruby は単にメソッドが多いだけではなく、メソッドの呼び方も豊富に用意されているところに多彩さを感じますね。

特化した方を選ぶメリット

特化した方を選ぶメリットですが、

  • 実引数やブロック引数を省略できることで、コードがシンプルになる (コード量が少し減る) 。
  • メソッド名がより直感的で読みやすくなる。

と言えると思います。

例えば inject(:+) より sum の方が、slice(0, 2) より first(2) の方が引数の数が減り、なおかつ直感的に読めると思います。

まとめ

Ruby では 1 つの処理を実装するのに複数の方法が存在することが多いです。多くのメソッドが提供され、また同じメソッドでもさらに複数の機能を備えていることが多いです。そこで 慣れない処理を実装した際は、より特化した方法が存在しないかリファレンスを調べてみる ことをおすすめします。Ruby ならきっとドラえもんのひみつ道具のように、うってつけな方法を提供してくれることでしょう3 :muscle_tone1:


  1. Ruby と同じスクリプト言語の Perl には "There's more than one way to do it (それをする方法はひとつじゃない)" という思想があり、Ruby と似ています。一方で、同じくスクリプト言語の Python には "There should be one-- and preferably only one --obvious way to do it (それをする明白な方法が一つあるはず。そして望ましいのは一つだけであること)" という正反対の思想があります。 

  2. かつて Ruby はよく 驚き最小の原則 (あるいは「Matz の驚き最小の原則」)とともに語られていたということですが、このように Ruby にはプログラマがほしいと思う機能が自然な形で存在するというのも理由のひとつなのでしょう。 

  3. もし Ruby にほしい機能が存在しない場合は、外部のライブラリである Active Support コア拡張機能 を調べてみるのも良いでしょう。Active Support とは Web フレームワークである Ruby on Rails の提供する一つの機能で、Array や Hash など Ruby の既存のクラスを拡張する形で便利なメソッドを提供しています。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby] 複数の方法がある場合はより特化した方を選ぼう (初心者向け)

はじめに

Ruby は非常に多彩な文法やクラス、メソッドを持っており、1 つの処理を実装するのに複数の方法が存在することが多いです1。そのため、どの方法を採用すればよいか迷ったり、あるいはよりよい方法がないか気になったりする機会も多いです。そこで、そういうケースで役立つささやかな指針を提案します。それが

ある処理を実装するために複数の方法がある場合は、より特化した方を選ぼう

です。

具体例

ケース 1: 整数の配列の合計値を求める

配列などの Enumerable (列挙可能) なクラスを操作する場合、Array#each や Enumerable#map に続いて Enumerable#inject の使い方を学ぶ方も多いと思います。リファレンスには inject メソッドを用いて整数の配列の合計値を求めるコードが記載されています。

[2, 3, 4, 5].inject { |result, item| result + item } #=> 14

さらに inject の使い方に詳しくなれば、次のように書くかもしれません。

[2, 3, 4, 5].inject(:+) #=> 14

しかし、ここで立ち止まってやりたいのは、より特化した方法があるのではないかと模索することです。Ruby にはあたかもプログラマの思考を先回りしたかのように、おあつらえ向きのメソッドを用意してくれていることがよくあります2

「合計を求めるのだから sum という名前のメソッドはないか」と Array のリファレンスで sum をページ内検索してみましょう。やはりありました :clap_tone1:

[2, 3, 4, 5].sum #=> 14

ケース 2: 配列の先頭から n 個の要素を取得する

次の配列から先頭の要素を 2 つだけ抜き出したいです。

girls = ['ゆの', '宮子', '沙英', 'ヒロ']

先頭の要素を 1 つだけ抜き出すなら

girls[0] #=> "ゆの"

と書いたり、あるいは Array#first を使用して

girls.first #=> "ゆの"

と書けます。わざわざ先頭の要素を取得することに特化したメソッドが first という名前で用意されている。まさに Ruby らしいですね!

では先頭から複数の値を取得するためにはどうしたらよいでしょうか。

例えば Ruby と同じスクリプト言語の Python なら、スライスと呼ばれる機能を使用して次のように書けます。

# Python の世界
>>> girls = ['ゆの', '宮子', '沙英', 'ヒロ']
>>> girls[0:2]  # スライスという機能を使って、0 番目 (先頭) から 2 つの要素を取得する。
['ゆの', '宮子']

しかし Ruby の配列にはスライスという機能はなく、同じ書き方をするとシンタックスエラーとなります。

girls[0:2] # SyntaxError

でも Ruby のことなので、必ず Python のスライスと同じような機能が存在するはずです!Ruby を信じるのです :pray:

今度は Array のリファレンスで slice をページ内検索してみましょう。Array#slice というメソッドが見つけるはずです。

girls.slice(0, 2) #=> ["ゆの", "宮子"]

お見事!さすが Ruby :clap_tone1:

ちなみに Array#[] というメソッド (Ruby では配列の [] もメソッドなのです) では実現できないのでしょうか。リファレンスを確認すると、引数を 2 つ渡す、あるいは Range オブジェクトを渡すことで実現できることがわかります。

girls[0, 2] #=> ["ゆの", "宮子"]
girls[0..1] #=> ["ゆの", "宮子"]

喜ぶのはまだ早いです。さらに別の方法も!先ほど登場した Array#first のリファレンスをよく読んでみてください。引数を指定することで挙動が変わるのです。

girls.first(2) #=> ["ゆの", "宮子"]

なんと先頭から 2 つの要素を取得できました。Ruby は単にメソッドが多いだけではなく、メソッドの呼び方も豊富に用意されているところに多彩さを感じますね。

特化した方を選ぶメリット

特化した方を選ぶメリットですが、

  • 引数やブロックを省略できることで、コードがシンプルになる (コード量が少し減る) 。
  • メソッド名がより直感的で読みやすくなる。

と言えると思います。

例えば inject(:+) より sum の方が、slice(0, 2) より first(2) の方が引数の数が減り、なおかつ直感的に読めると思います。

また

  • 他の方法よりパフォーマンスが向上していたり不具合が解消されていたりする。

というメリットが存在する場合もあります3

まとめ

Ruby では 1 つの処理を実装するのに複数の方法が存在することが多いです。多くのメソッドが提供され、また同じメソッドでもさらに複数の機能を備えていることが多いです。そこで 慣れない処理を実装した際は、より特化した方法が存在しないかリファレンスを調べてみる ことをおすすめします。Ruby ならきっとドラえもんのひみつ道具のように、うってつけな方法を提供してくれることでしょう4 :muscle_tone1:


  1. Ruby と同じスクリプト言語の Perl には "There's more than one way to do it (それをする方法はひとつじゃない)" という思想があり、Ruby と似ています。一方で、同じくスクリプト言語の Python には "There should be one-- and preferably only one --obvious way to do it (それをする明白な方法が一つあるはず。そして望ましいのは一つだけであること)" という正反対の思想があります。 

  2. かつて Ruby はよく 驚き最小の原則 (あるいは「Matz の驚き最小の原則」)とともに語られていたということですが、このように Ruby にはプログラマがほしいと思う機能が自然な形で存在するというのも理由のひとつなのでしょう。 

  3. Array#sum もこの例に該当します。:point_right_tone1: Ruby 2.4で追加されるArray#sumで配列の要素の総和が正確かつ高速になる 

  4. もし Ruby にほしい機能が存在しない場合は、外部のライブラリである Active Support コア拡張機能 を調べてみるのも良いでしょう。Active Support とは Web フレームワークである Ruby on Rails の提供する一つの機能で、Array や Hash など Ruby の既存のクラスを拡張する形で便利なメソッドを提供しています。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goでソケット通信

口上

そういえばソケット通信とかの、ネットワークプログラミングの基本を全然理解していなかったので、入門中のgoでとりあえず書いてみた。
ググってもいまいち書きたい仕様にマッチするものがなかったので、試行錯誤でできた内容を、備忘録兼ねて記載。

サーバー

まずはサーバー側。

server.go
package main

import (
    "fmt"
    "net"
    utilsErr "sample/utils/error"
    utilsNet "sample/utils/net"
    "time"
)

func main() {

    // 意味はないが雰囲気を出すため、敢て自分のIPを表示
    myIP, _ := utilsNet.GetMyLocalIP()
    fmt.Println("my ip address is now ...", myIP)

    // 50030ポート(適当)でメッセージを受け取る
    port := ":50030"
    protocol := "tcp"
    tcpAddr, err := net.ResolveTCPAddr(protocol, port)
    utilsErr.CheckWithExit(err)
    listner, err := net.ListenTCP(protocol, tcpAddr)
    utilsErr.CheckWithExit(err)
    fmt.Println("waiting for the connection ...")
    for {
        conn, err := listner.Accept()
        if err != nil {
            continue
        } else {
            fmt.Println("connected by .. ", conn.RemoteAddr().String())
        }

        // ゴルーチンほんと便利
        go handleClient(conn)
    }
}

// 送信されたバイトからメッセージを取り出して表示する
func handleClient(conn net.Conn) {
    defer conn.Close()

    conn.SetReadDeadline(time.Now().Add(10 * time.Second))

    messageBuf := make([]byte, 1024)
    messageLen, err := conn.Read(messageBuf)
    utilsErr.CheckWithExit(err)
    message := string(messageBuf[:messageLen])
    fmt.Println(message)

    // この書き込み処理がないと、連続してclientからメッセージを送信すると以下のエラーが発生する
    // fatal: error: dial tcp 192.168.3.22:50031->192.168.3.22:50030: bind: address already in useexit status 1
    // 理由は理解していない・・
    conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
    conn.Write([]byte(message))
}

最後のところ、いったんnet.Connから読み出したメッセージを、再度Connに書き込まないと、クライアントから2回め以降に接続した際にコメントのエラーが発生する。
ちゃんと調べれてないが、書き込むことで接続が一旦クリアされている?

クライアント

次、クライアント側。

client.go
package main

import (
    "fmt"
    "net"
    "os"
    utilsErr "sample/utils/error"
    utilsNet "sample/utils/net"
    "time"
)

func main() {
    // メッセージ用の引数を一つ受け取る仕様
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s message", os.Args[0])
        os.Exit(1)
    }
    message := os.Args[1]

    // 今回はサーバー/クライアント共にローカルマシン
    // ポートは適当に
    serverIP, _ := utilsNet.GetMyLocalIP()
    serverPort := "50030"
    clientIP, _ := utilsNet.GetMyLocalIP()
    clientPort := 50031

    // tcpで接続
    serverAdd, err := net.ResolveTCPAddr("tcp", serverIP+":"+serverPort)
    utilsErr.CheckWithExit(err)
    clientAddr := new(net.TCPAddr)
    clientAddr.IP = net.ParseIP(clientIP)
    clientAddr.Port = clientPort
    conn, err := net.DialTCP("tcp", clientAddr, serverAdd)
    utilsErr.CheckWithExit(err)
    defer conn.Close()

    // メッセージをバイトに変換して送信
    conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
    conn.Write([]byte(message))
}

ポイントは、
・コマンドライン引数で送信したいメッセージを渡す
・クライアント自身のIPを動的に取得する(後述のGetMyLocalIPメソッド)
・今回はローカルマシン内の動作なので、サーバーIPもローカルアドレスになる
くらいかな。

ユーティリティー達

一応載せとく。

ipUtil.go
package net

import (
    "errors"
    "net"
)

// ローカルIPアドレスを、文字列で取得する
//
// ・すべてのネットワークインターフェースを走査しする
// ・ループバックアドレスは無視する
// 出展:https://codeday.me/jp/qa/20190120/147217.html
func GetMyLocalIP() (string, error) {
    ifaces, err := net.Interfaces()
    if err != nil {
        return "", err
    }

    for _, iface := range ifaces {
        if iface.Flags&net.FlagUp == 0 {
            continue // interface down
        }
        if iface.Flags&net.FlagLoopback != 0 {
            continue // loopback interface
        }

        addrs, err := iface.Addrs()
        if err != nil {
            return "", err
        }
        for _, addr := range addrs {
            var ip net.IP
            switch v := addr.(type) {
            case *net.IPNet:
                ip = v.IP
            case *net.IPAddr:
                ip = v.IP
            }
            if ip == nil || ip.IsLoopback() {
                continue
            }
            ip = ip.To4()
            if ip == nil {
                continue // not an ipv4 address
            }
            return ip.String(), nil
        }
    }
    return "", errors.New("are you connected to the network?")
}
errUtil.go
package error

import (
    "fmt"
    "os"
)

func CheckWithExit(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "fatal: error: %s", err.Error())
        os.Exit(1)
    }
}

使い方

# サーバー起動
$ go run server.go

# クライアントからメッセージを送る
$ go run client/client.go "こんにちは!"
$ go run client/client.go "今日はいい天気ですね"
$ go run client/client.go "さようなら〜"

結果

出展

https://golang.org/pkg/net/
http://kudohamu.hatenablog.com/entry/2014/11/03/071802
https://codeday.me/jp/qa/20190120/147217.html
※サンプルの仕様は、こちらの本に出てくるソケット通信のサンプル(Python)を元にした。

次は、この知識を元に、goでもう少し複雑なP2Pプログラムを書いてみる予定。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む