Rubyの引数いろいろ

昨年のクリスマスにRuby3.0がリリースされました。8年ぶりのメジャーバージョンアップで、その目玉として静的型付けが導入されたことはご存知の方も多いのではないでしょうか。
それ以外にも様々な変更がありましたが、そのうちの一つでキーワード引数の扱いが変更になりました。引数は種類が多くてややこしいですよね。私もよくわからなくなるのでまとめてみようと思います。

引数の書き方

Rubyは関数呼び出しや、定義の時に引数のカッコを省略可能です。

def greet name
  puts "Hello, #{name}!"
end

greet "Taro" #=> "Hello, Taro!"

と書いても、

def greet(name)
  puts "Hello, #{name}!"
end

greet("Taro") #=> "Hello, Taro!"

と書いてもOKです。私はカッコをつける派ですが、付けないコードもよく見かけます。

デフォルト引数

引数に初期値を持たせることができます。これによりメソッド呼び出し時に、引数が必須ではなくなります。

def greet(name="Taro")
  puts "Hello, #{name}!"
end

greet #=> "Hello, Taro!"
greet("Jiro") #=> "Hello, Jiro!"

def greet(name)
  puts "Hello, #{name}!"
end

greet #=> ArgumentError

デフォルト引数と普通の引数の両方を渡したい場合は、引数の個数からRubyが推測をしてくれます。

def greet(name="Taro", age)
  puts "#{name} is #{age}."
end

greet(10) #=> "Taro is 10."
greet("Jiro", 10) #=> "Jiro is 10."

可変長引数

引数の個数が決まっていない場合に使用します。名前の通りですね。

def greet(*name)
  name.each do |name|
    puts "Hello, #{name}!" 
  end
end

greet("Taro", "Jiro") #=> "Hello, Taro" "Hello, Jiro"

引数の前に*を付けます。これで複数の引数を配列として受け取れます。配列なので、ループ等で処理してあげます。

可変長引数と、普通の引数の両方を渡したい場合は、デフォルト引数の場合と同様、引数の数からRubyが推測してくれます。

def greet(*name, msg)
  name.each do |name|
    puts "#{msg}, #{name}!"
  end
end

greet("Taro", "Jiro", "Hello") #=> "Hello, Taro!" "Hello, Jiro!"

オプション引数

可変長引数に類似の引数です。引数にキーとバリューの組み合わせを指定したい場合に使用します。引数は可変長です。

def greet(**args)
  args.each do |arg|
    puts "#{arg[0].capitalize} is #{arg[1]}."
  end
end

greet(name: "Taro", age: 10) #=> "Name is Taro." "Age is 10."

引数の前に**を付けます。これで複数のキーとバリューの組み合わせをハッシュとして受け取れます。指定するキーはなんでも良いので使い道は限定されると思います。あらかじめキーだけは指定しておくキーワード引数の方が使用頻度は高いと思います。

キーワード引数

引数にキーとバリューの組み合わせを指定します。引数が複数あると、何をどの順に渡しているのか分かりにくくなります。そういう場合はキーワード引数を使います。呼び出す時は、キーとバリューをセットで渡します。オプション引数との違いは、キーはあらかじめ指定しておく必要があるということです。

def greet(name:, age:)
  puts "#{name} is #{age}."
end

greet(name: "Taro", age: 10) #=> "Taro is 10."

どの引数がどういう意味を持つのかが一目瞭然だと思います。

キーワード引数はデフォルト引数を渡すこともできます。

def greet(name: "Taro", age: 10)
  puts "#{name} is #{age}."
end

greet #=> "Taro is 10."
greet(name: "Jiro") #=> "Jiro is 10."

キーワード引数はRuby2.0から導入された機能で、それ以前はハッシュを使ってキーワード引数を擬似的に実現していました。

def greet(options = {})
  name = options[:name]
  age = options[:age]

  puts "#{name} is #{age}."
end

greet(name: "Taro", age: 10) #=> "Taro is 10."

引数にキーとバリューの組み合わせを指定する場合、明示的にハッシュで書いても、{}を省略した形で書いても暗黙的に双方向に変換されます。

# キーワード引数の場合
def greet(name: "Taro", age: 10)
  puts "#{name} is #{age}."
end

profile = {name: "Jiro", age: 20}
greet(profile) #=> "Jiro is 20."
greet(name: "Jiro", age: 20) #=> "Jiro is 20."


# 普通の引数でハッシュを受け取る場合
def greet(options = {})
  name = options[:name]
  age = options[:age]

  puts "#{name} is #{age}."
end

greet(profile) #=> "Jiro is 20."
greet(name: "Jiro", age: 20) #=> "Jiro is 20."

しかしRuby3.0からは、この仕様が変更になっています。キーワード引数の扱いが独立し、キーワード引数に{}をつけた場合に動かなくなりました。(普通の引数でハッシュを受け取る場合は互換性が維持されています。)従って明示的に、ハッシュを展開する必要があります。**をつければOKです。

def greet(name: "Taro", age: 10)
  puts "#{name} is #{age}."
end

profile = {name: "Jiro", age: 20}
greet(profile) #=> Ruby3.0からエラー
greet(**profile) #=> "Jiro is 20."

***

*Splat Operator)は配列を展開する演算子**Double Splat Operator)はハッシュを展開する演算子です。 Javascriptで言うところの...Spread Operator)のような感じだと思います。

array = [1, 2, 3]
hash = {a: 1, b: 2}

[a, b, array, c] #=> [a, b, [1, 2, 3], c]
[a, b, *array, c] #=> [a, b, 1, 2, 3, c]

{A: 10, B: 11, hash, C: 12} #=> SyntaxError
{A: 10, B: 11, **hash, C: 12} #=> {A: 10, B: 11, a: 1, b: 2, C: 12}

***を使うと配列やハッシュを展開できるので、逆も可能です。これを利用したのが可変長引数、オプション引数です。 メソッド定義の際に*nameのように引数を指定すると、呼び出すときに引数が配列に変換されるという仕組みです。

ブロック引数

引数にブロックを指定できます。定義する際は&を引数の前に書きます。メソッド内ではyieldでブロックを呼び出します。

def greet(&block)
  yield
end

greet { puts "hello" } #=> "hello"

ブロック引数にロックを渡したい場合は、呼び出しの際も&を書いてやればOKです。

def greet(&block)
  puts block.yield("hello")
end

proc_obj = proc { |str| str.upcase }
greet(&proc_ojb)

定義時に引数にブロックを指定していないメソッドにもブロックは渡すことができます。ただ、注意点としては呼び出し方はyieldのみ限定されます。callで呼び出すことはできません。

def greet
  if block_given?
    yield
  else
    puts "No block"
  end
end

proc_obj = -> do
  File.write("hoge.txt", "Hello, world")
  File.read("hoge.txt")
end

greet(&proc_obj) #=> "Hello, world"
greet #=> "No block"

if block_given?で引数にブロックが渡ってきているか条件分岐が可能です。

ブロックの呼び出し方いろいろ

block.call
block.yeild
yeild

procはオブジェクトの作り方も、呼び出し方も何パターンもあるのでまた別に書きたいと思います。

参考
https://techlife.cookpad.com/entry/2020/12/25/155741
https://qiita.com/jnchito/items/74e0930c54df90f9704c
https://qiita.com/rtoya/items/33617078501776fdcad7
https://ryotatake.hatenablog.com/entry/2018/11/20/splat_operator

CORSをちゃんと理解する

今回はCORSについてまとめたいと思います。私はよくサーバー側の設定を忘れて、ブラウザに怒られたりしてますが、今ひとつ理解できず、コピペして終わってました。Railsapiモードでアプリを作る時とかも、よくわからずgem rack-corsとか書いてました。最近、FetchAPIについて学ぶ機会があって、ブラウザからのhttp通信についてちゃんと調べようと思ったのがきっかけでCORSについてもまとめてみました。

corsとは?

CORS(Cross-Origin Resource Sharing)は異なるオリジン間での通信を許可する仕組み。CSRFなどのセキュリティ面から、XMLHttpRequestFetchAPIなどのhttp通信は同一オリジンポリシーに従います。従ってオリジンが異なる場合はそれを許可するための、適切なcorsに関するヘッダーが必要になってきます。

オリジンとは?

オリジン = ドメイン + プロトコル + ポート番号

https://domain-a.comみたいな部分がオリジンですね。つまり、https://domain-a.comからhttps://domain-b.comへの通信はデフォルトでは許可されず、corsによる設定が必要になるという事です。

ではcorsの設定についてみていきます。

プリフライトリクエス

後述する特定の条件以外の場合は、プリフライトリクエストと呼ばれるリクエストを最初に行い、あらかじめサーバーがリクエストに応答可能か確認を行います。httpのOPTIONSリクエストによって、これを行います。

リクエス

OPTIONS / HTTP/1.1
Origin: https://domain-a.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Custom-Header, Content-Type

通信を行いたいメソッドや、ヘッダーをサーバーに対して提示します。Access-Control-Request-Headersに関しては後述の単純リクエストで許可されるヘッダー以外を指定します。

レスポンス

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://domain-a.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-Custom-Header, Content-Type
Access-Control-Max-Age: 86400

レスポンスには許可するオリジン、メソッド、ヘッダー情報が含まれます。Access-Control-Allow-Originに関してはワイルドカードの使用が可能なので、全てを許可する場合は、

Access-Control-Allow-Origin: *

と書くことが可能です。
Access-Control-Max-Ageは、プリフライトリクエストの結果をキャッシュしておける上限時間です。

単純リクエス

以下のいずれかの条件を満たす場合は、プリフライトリクエストを送らずに直接http通信を行うことが可能です。
逆に言えば、この条件を満たさない場合は、全てまず、プリフライトリクエストを行わなければなりません。

  • 以下のメソッドのいずれかである。
    GET, HEAD, POST

  • 以下のヘッダーのみを含む。
    Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width

  • Content-Typeヘッダーは以下のみ。
    application/x-www-form-urlencoded, multipart/form-data, text/plain

  • リクエストに使用される XMLHttpRequestUpload にイベントリスナーが登録されていない。

  • リクエストに ReadableStream オブジェクトが使用されていない。

資格情報(認証)

認証を行うためにCookieをヘッダーに含めて送信したい場合は多くあると思います。デフォルトではヘッダーにCookieは含めないので、リクエスト時に設定が必要です。 XMLHttpRequestの場合

const xhr = new XMLHttpRequest();
xhr.withCredentials = true;

FetchAPIの場合

fetch('https://domain-a.com', {
  mode: 'cors',
  credentials: 'include',
});

これにより、リクエストヘッダーにCookieヘッダーが含まれるようになります。
レスポンスに次のヘッダーが含まれば、許可されたことになります。

Access-Control-Allow-Credentials: true

これはプリフライトリクエストの場合でも、実際のリクエストの場合にも(もちろん単純リクエストの場合も)含まれます。プリフライトリクエストの場合は、実際のリクエストにCookieを含められるかどうか、実際のリクエストの場合は、それをブラウザで利用して良いかを定めています。つまり、実際のリクエストでこれがtrueでなかった場合は、ブラウザによって無視されるということになります。

注意点としては、資格情報を送る場合はAccess-Control-Allow-Originワイルドカードを設定できません。特定のオリジン名を指定する必要があります。

参考
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS http://www.tohoho-web.com/ex/cors.html https://qiita.com/att55/items/2154a8aad8bf1409db2b

Socket.io + Express を試してみる

WebSocketとは?

双方向通信を可能にするプロトコルの一つです。リアルタイムで通信を行いたいチャットなどに利用されます。

普通のhttp/https通信では、クライエントからリクエストが送られる度にコネクションが張られ、サーバーからレスポンスが返ると、コネクションは切断されます。 つまり、通信の度にコネクションを張り直す必要があり、クライエントからのリクエストなしにサーバーからレスポンスを送信することができません。

これを解決するために、従来はロングポーリング(comet)という技術が使われていました。 一定の間隔でブラウザからリクエストを送り、サーバーは新着情報がなければレスポンスを一定時間保留にします。その間に新着があればそれを返し、なければ何もなかったというレスポンスを返します。サーバー側の保留なしの単純なポーリングよりリアルタイム性が高いのが特徴です。これによって擬似的に、リアルタイム通信が可能になりますが、以下の欠点があります。

  • ヘッダーが冗長でオーバーヘッドが大きい
  • タイムアウトがある
  • httpコネクションを占有してしまう

一方でWebsocketは、最小2byte, 最大でも14byteと軽量かつ、HTTPをアップグレーデして、TCP上で双方向通信を実現します。 クライエントからの最初のリクエストのヘッダーは次のようになります。

GET / HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: xxxxxxxxxxx==
Sec-WebSocket-Version: 13

サーバーからのレスポンスは次のようになります。

HTTP/1.1 101 OK
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: yyyyyyyyyy=

なんとなく直感で理解できるのではないでしょうか。
Sec-Websocket-KeySec-Websocket-Acceptはそれぞれ対応しており、クライエントが自分のリクエストに対するレスポンスだと認識できるようになっています。 ちなみに、101は、Upgradeリクエストヘッダーが送られてきた場合に返却するSwitchinig Protocols レスポンスです。 この時点で、Websocketコネクションが確立されました。

Socket.ioとは?

手軽にwebsocketを実現するjavascriptライブラリで、フロント側と、node.jsで実装されたサーバー側から作られています。 WebSocketが繋がらなくなった時に、ロングポーリングポーリングなどの代替手段を提供してくれる他、到達確認や認可、namespaceなどを提供してくれます。

チャットを作ってみる

まずexpresssocket.ioをインストールします。

yarn add express socket.io

次のようにapp.jsを作成します。

// app.js

const express = require('express');
const app = express();
const http = require('http').createServer(app);

const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send('<h1>Hello world</h1>');
});

http.listen(PORT, () => {
  console.log(`Server listening. Port: ${PORT}`);
});

expressに関する記述のみですが、動作確認をしておきましょう。

node app.js

ブラウザで、localhost:3000にアクセスします。Hello worldと表示されるはずです。

次に表示部分をhtmlファイルに分けていきます。

// app.js

app.use(express.static(`${__dirname}/public`));

app.get('/', (req, res) => {
  res.send(`${__dirname}/index.html`);
});

まず、app.jsでファイルを読み込みます。静的ファイルを分けたいので、staticを使っています。

<!-- public/index.html -->

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Socket.io</title>
    <link rel="stylesheet" href="index.css">
  </head>
  <body>
    <div class="chat-field">
      <div class="chat-area">
        <ul id="messages"></ul>
      </div>
      <form class="chat-form" action="">
        <input id="m" />
        <button class="disabled">Send</button>
      </form>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    <script type="text/javascript" src="index.js"></script>

  </body>
</html>
/* public/index.css */

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font: 13px Helvetica, Arial;
}

.chat-field {
  margin: 10em auto;
  width: 40%;
  height: 50em;
  border-radius: 5px;
  box-shadow: 5px 5px 25px 0 rgba(0, 0, 0, .5);
  text-align: center;
}

.chat-area {
  margin: 5em auto;
  width: 90%;
  height: 40em;
  overflow: scroll;
}

.chat-area li {
  margin: 0.5em;
  padding: 0.7em;
  background-color: rgb(223, 223, 223);
  border-radius: 8px;
  list-style-type: none;
}

.chat-form input {
  margin-right: 0.3em;
  padding: 0.6em;
  width: 70%;
  border: 2px solid blueviolet;
  border-radius: 5px;
}

.chat-form button {
  padding: 0.6em;
  border: 2px solid blueviolet;
  border-radius: 5px;
  background-color: blueviolet;
  color: white;
}

次にindex.jsを書いていきます。アクセス時に、websocketを張り、送信ボタンを押すとそれを通じて通信を行い、レスポンスを表示する仕様です。

// public/index.js

$(() => {
  $('input').change(function() {
    let text = $(this).val();
    if (text === null) {
      $('button').prop('disabled', true);
    } else {
      $('button').prop('disabled', false);
    };
  });

  const socket = io();
  $('form').submit(e => {
    e.preventDefault();

    socket.emit('post', $('#m').val());
    $('#m').val('');
    $('button').prop('disabled', true);

    return false;
  });

  socket.on('msg', res => {
    $('#messages').append($('<li>').text(res));
  });
});

socket.ioインスタンスに対して、emitで送信、onで受信を行っています。第一引数は、送信側と受信側で同じキーを指定します。 ここまでで一度動作確認をしておきましょう。次のような画面になっていれば成功です。 f:id:shyu_61:20201220135226p:plain

続いてサーバー側にsocket.io部分を書いていきます。

// app.js

const io = require('socket.io')(http);

io.on('connection', socket => {
  console.log('user connected');

  socket.on('post', post => {
    io.emit('msg', post);
  });
});

onで受信し、postをキーに持つメッセージを受け取ります。そして、受け取ったものをそのまま返却していますが、これはconnectionが張られている全てのユーザーに送信されます。動作確認してみてください。

これで簡単なチャットは作成できましたが、自分と相手のメッセージくらい区別できるようにしておきましょう。 具体的には、接続時にユーザー一意のトークンを発行し、ユーザーがリクエストの際にそのトークンを含めることで実装します。

トークン生成にはcryptoを使用し、socket.idでユーザーごとに割り振られたidを取得できるのでこれを元に作成します。

// app.js

const crypto = require('crypto');

io.on('connection', socket => {
  console.log('user connected');

  (() => {
    const token = makeToken(socket.id);
    io.to(socket.id).emit('token', { token: token });
  })();

  socket.on('post', post => {
    io.emit('msg', post);
  });
});

const makeToken = id => {
  return crypto.createHash('sha1').update(id).digest('hex');
};

全てのユーザーに送信者のトークンを返却してるのが相当イケてないですが今回はよしとしてください(笑)

最後にクライエント側で識別を行って、ラインのように左右に出し分けたいと思います。

// index.js

const IAM = { token: null };
socket.on('token', data => {
  IAM.token = data.token;
});

$('form').submit(e => {
  e.preventDefault();

  socket.emit('post', {
    msg: $('#m').val(),
    token: IAM.token,
  });

  $('#m').val('');
  $('button').prop('disabled', true);

  return false;
});

socket.on('msg', res => {
  if (res.token === IAM.token) {
    $('#messages').append($('<li class="mine">').text(res.msg));
  } else {
    $('#messages').append($('<li class="others">').text(res.msg));
  }
});

まず、tokenを受け取り、IAMにセットしておきます。(tokenはconnectionが張られた際に、サーバーから送信されるものです。) メッセージの送信時に、トークンを含めるように変更し、受け取り時はIAMと比較して自分が送信したものかどうか判定します。class属性を変えることで表示を分けます。

最後にindex.cssを変更します。

/* index.css */

.chat-area li {
  padding: 0.7em;
  background-color: rgb(223, 223, 223);
  border-radius: 8px;
  list-style-type: none;
}

.mine {
  margin: 0.5em;
  width: 50%;
}

.others {
  margin: 0.5em 0.5em 0.5em auto;
  width: 50%;
}

これで完成です。ブラウザで複数タブを開いて、チャットをやりとりしてみてください。 最終コードはgithubにあげてます。 f:id:shyu_61:20201220135633p:plain

参考にさせて頂きましたm

WebSocketについて調べてみた。 - Qiita
socket.io が提供してくれているものは何か - from scratch
はじめてのSocket.io #2 チャット編「自分がemitした通信内容か判定する」