diff --git a/app/pagesList.ts b/app/pagesList.ts index 7df1274..8140446 100644 --- a/app/pagesList.ts +++ b/app/pagesList.ts @@ -20,6 +20,25 @@ export const pagesList = [ { id: 9, title: "ジェネレータとデコレータ" }, ], }, + { + id: "ruby", + lang: "Ruby", + description: "hoge", + pages: [ + { id: 1, title: "rubyの世界へようこそ" }, + { id: 2, title: "基本構文とデータ型" }, + { id: 3, title: "制御構造とメソッド定義" }, + { id: 4, title: "すべてがオブジェクト" }, + { id: 5, title: "コレクション (Array, Hash, Range)" }, + { id: 6, title: "ブロックとイテレータ" }, + { id: 7, title: "クラスとオブジェクト" }, + { id: 8, title: "モジュールとMix-in" }, + { id: 9, title: "Proc, Lambda, クロージャ" }, + { id: 10, title: "標準ライブラリの活用" }, + { id: 11, title: "テスト文化入門" }, + { id: 12, title: "メタプログラミング入門" }, + ], + }, { id: "cpp", lang: "C++", diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index 3caa615..8b98bbe 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -47,7 +47,13 @@ export function ExecFile(props: ExecProps) { terminalInstanceRef.current!.write(systemMessageColor("実行中です...")); const outputs = await runFiles(props.filenames); clearTerminal(terminalInstanceRef.current!); - writeOutput(terminalInstanceRef.current!, outputs, false); + writeOutput( + terminalInstanceRef.current!, + outputs, + false, + undefined, + props.language + ); // TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる setExecResult(props.filenames.join(","), outputs); setExecutionState("idle"); @@ -60,6 +66,7 @@ export function ExecFile(props: ExecProps) { runFiles, setExecResult, terminalInstanceRef, + props.language, ]); return ( diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index 94bace1..8b30101 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -60,7 +60,7 @@ const sampleConfig: Record = { }, ruby: { repl: true, - replInitContent: '>> puts "Hello, World!"\nHello, World!', + replInitContent: 'irb(main):001:0> puts "Hello, World!"\nHello, World!', editor: { "main.rb": 'puts "Hello, World!"', }, diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index b74e986..3e16fb1 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -28,7 +28,9 @@ export type SyntaxStatus = "complete" | "incomplete" | "invalid"; // 構文チ export function writeOutput( term: Terminal, outputs: ReplOutput[], - endNewLine: boolean + endNewLine: boolean, + returnPrefix: string | undefined, + language: RuntimeLang ) { for (let i = 0; i < outputs.length; i++) { const output = outputs[i]; @@ -47,6 +49,12 @@ export function writeOutput( case "system": term.write(systemMessageColor(message)); break; + case "return": + if (returnPrefix) { + term.write(returnPrefix); + } + term.write(highlightCodeToAnsi(message, language)); + break; default: term.write(message); break; @@ -77,7 +85,7 @@ export function ReplTerminal({ checkSyntax, splitReplExamples, } = useRuntime(language); - const { tabSize, prompt, promptMore } = langConstants(language); + const { tabSize, prompt, promptMore, returnPrefix } = langConstants(language); if (!prompt) { console.warn(`prompt not defined for language: ${language}`); } @@ -160,12 +168,18 @@ export function ReplTerminal({ const handleOutput = useCallback( (outputs: ReplOutput[]) => { if (terminalInstanceRef.current) { - writeOutput(terminalInstanceRef.current, outputs, true); + writeOutput( + terminalInstanceRef.current, + outputs, + true, + returnPrefix, + language + ); // 出力が終わったらプロンプトを表示 updateBuffer(() => [""]); } }, - [updateBuffer, terminalInstanceRef] + [updateBuffer, terminalInstanceRef, returnPrefix, language] ); const keyHandler = useCallback( @@ -184,17 +198,10 @@ export function ReplTerminal({ } } else if (code === 13) { // Enter - const hasContent = - inputBuffer.current[inputBuffer.current.length - 1].trim() - .length > 0; const status = checkSyntax ? await checkSyntax(inputBuffer.current.join("\n")) : "complete"; - if ( - (inputBuffer.current.length === 1 && status === "incomplete") || - (inputBuffer.current.length >= 2 && hasContent) || - !isLastChar - ) { + if (status === "incomplete" || !isLastChar) { // 次の行に続く updateBuffer(() => [...inputBuffer.current, ""]); } else { diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx index d72c7a0..65bb14c 100644 --- a/app/terminal/runtime.tsx +++ b/app/terminal/runtime.tsx @@ -30,6 +30,7 @@ export interface LangConstants { tabSize: number; prompt?: string; promptMore?: string; + returnPrefix?: string; } export type RuntimeLang = "python" | "ruby" | "cpp" | "javascript"; @@ -99,8 +100,10 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants { case "ruby": return { tabSize: 2, - prompt: ">> ", - promptMore: "?> ", + // TODO: 実際のirbのプロンプトは静的でなく、(main)や番号などの動的な表示がある + prompt: "irb> ", + promptMore: "irb* ", + returnPrefix: "=> ", }; case "javascript": return { diff --git a/app/terminal/worker/ruby.ts b/app/terminal/worker/ruby.ts index a8e1ff2..e75b5cb 100644 --- a/app/terminal/worker/ruby.ts +++ b/app/terminal/worker/ruby.ts @@ -21,13 +21,26 @@ export function useRuby() { function splitReplExamples(content: string): ReplCommand[] { const initCommands: { command: string; output: ReplOutput[] }[] = []; for (const line of content.split("\n")) { - if (line.startsWith(">> ")) { - // Ruby IRB uses >> as the prompt - initCommands.push({ command: line.slice(3), output: [] }); - } else if (line.startsWith("?> ")) { - // Ruby IRB uses ?> for continuation + if (line.startsWith("irb")) { + if ( + initCommands.length > 0 && + initCommands[initCommands.length - 1].output.length === 0 + ) { + // 前のコマンド行と連続している場合つなげる + initCommands[initCommands.length - 1].command += + "\n" + line.slice(line.indexOf(" ") + 1); + } else { + initCommands.push({ + command: line.slice(line.indexOf(" ") + 1), + output: [], + }); + } + } else if (line.startsWith("=> ")) { if (initCommands.length > 0) { - initCommands[initCommands.length - 1].command += "\n" + line.slice(3); + initCommands[initCommands.length - 1].output.push({ + type: "return", + message: line.slice(3), + }); } } else { // Lines without prompt are output from the previous command diff --git a/public/docs/python-1.md b/public/docs/python-1.md index 0990183..d3e744d 100644 --- a/public/docs/python-1.md +++ b/public/docs/python-1.md @@ -1,7 +1,22 @@ -# 第1章: Pythonへようこそ:環境構築と基本 +# 第1章: Pythonへようこそ:環境構築と基本思想 プログラミング経験者であっても、言語ごとのツールや流儀を最初に理解することは重要です。この章では、Pythonの開発環境を整え、基本的なツールの使い方を学びます。 +## Pythonの思想と特徴: 「読みやすさ」は最優先 + +他の言語(Java, C++, PHPなど)と比較したとき、Pythonが最も重視するのは**コードの可読性(Readability)**です。 + +* **シンプルな文法:** C言語やJavaのような `{}`(波括弧)によるブロックや、行末の `;`(セミコロン)を必要としません。 +* **強制的なインデント:** Pythonは、**インデント(字下げ)**そのものでコードブロックを表現します。これは構文的なルールであり、オプションではありません。これにより、誰が書いても(ある程度)同じような見た目のコードになり、可読性が劇的に向上します。 +* **動的型付け (Dynamic Typing):** JavaやC++のように `int num = 10;` と変数の型を明示的に宣言する必要がありません。`num = 10` と書けば、Pythonが実行時に自動的に型を推論します。(これはJavaScriptやPHPと似ていますが、Pythonは型付けがより厳格(Strong Typing)で、例えば文字列と数値を暗黙的に連結しようとするとエラーになります) +* **豊富な標準ライブラリ**: 「Batteries Included(バッテリー同梱)」という思想のもと、OS操作、ネットワーク、データ処理、JSON、正規表現など、多くの機能が最初から標準ライブラリとして提供されています。 + +**💡 The Zen of Python (Pythonの禅)** Pythonの設計思想は、`import this` というコマンドでいつでも確認できます。 + +* Beautiful is better than ugly. (醜いより美しいほうがいい) +* Explicit is better than implicit. (暗黙的より明示的なほうがいい) +* Simple is better than complex. (複雑であるよりシンプルなほうがいい) + ## Pythonのインストール方法 手元の環境で本格的に開発を進めるために、Pythonのインストール方法を紹介します。 diff --git a/public/docs/ruby-1.md b/public/docs/ruby-1.md new file mode 100644 index 0000000..4d10219 --- /dev/null +++ b/public/docs/ruby-1.md @@ -0,0 +1,147 @@ +# 第1章: Rubyの世界へようこそ - 環境構築と第一歩 + +Rubyへようこそ! 他の言語でのプログラミング経験をお持ちのあなたなら、Rubyの持つ柔軟性と「開発者の楽しさ」をすぐに感じ取れるはずです。この章では、Rubyの基本的な哲学に触れ、あなたのマシンに開発環境をセットアップし、最初のコードを実行します。 + +## Rubyの哲学と特徴 + +Rubyは、まつもとゆきひろ(Matz)氏によって開発された、**シンプルさ**と**生産性**を重視した動的オブジェクト指向言語です。 + + * **すべてがオブジェクト (Everything is an Object)** + JavaやPythonでは`int`や`float`などのプリミティブ型がオブジェクトとは別に存在しますが、Rubyでは**すべてがメソッドを持つオブジェクト**です。`5`のような数値や`"hello"`のような文字列はもちろん、`nil`(nullに相当)や`true`/`false`さえもオブジェクトです。 + + ```ruby-repl:1 + irb(main):001> 5.class + => Integer + irb(main):002> "hello".upcase + => "HELLO" + irb(main):003> nil.nil? + => true + ``` + + * **開発者を楽しませる (MINASWAN)** + Rubyの設計思想の核は、プログラマがストレスなく、楽しくコーディングできることを最適化する点にあります。これはしばしば「**MINASWAN** (Matz Is Nice And So We Are Nice)」というコミュニティの標語にも表れています。言語仕様が厳格さよりも「驚き最小の原則」や表現力を優先することがあります。 + + * **柔軟性と表現力** + Rubyは非常に柔軟で、既存のクラスを後から変更する(オープンクラス)ことや、コードによってコードを操作するメタプログラミングが容易です。これにより、Ruby on Railsのような強力なフレームワークや、RSpecのようなDSL(ドメイン固有言語)が生まれています。 + + +## 他言語との簡単な比較 + +あなたの経験言語とRubyを少し比べてみましょう。 + + * **Pythonistaへ**: + + * インデントは構文的な意味を持ちません(単なる可読性のため)。 + * ブロック(コードのまとまり)は`:`とインデントではなく、`do...end`キーワード、または`{...}`(ブレース)で定義します。 + * メソッド呼び出しの丸括弧`()`は、曖昧さがなければ省略可能です。`print "hello"`のように書けます。 + + * **Java/C\# Developerへ**: + + * Rubyは静的型付けではなく**動的型付け**です。変数の型宣言(`int i = 10`)は不要で、`i = 10`と書くだけです。 + * コンパイルステップは不要です。スクリプトとして直接実行されます。 + * `public`, `private`, `protected`のアクセス修飾子はありますが、Javaなどとは少し動作が異なります(特に`private`)。 + + * **JavaScript Developerへ**: + + * Rubyもクラスベースのオブジェクト指向です(ただし、内部的にはJSのプロトタイプチェーンに似た特異クラスの仕組みも持ちます)。 + * `this`の代わりに`self`を使いますが、`self`のコンテキストはJSの`this`ほど複雑に変化せず、より直感的です。 + * `true`, `false`, `nil` 以外はすべて「Truthy(真)」として扱われます(`0`や空文字列`""`も真です)。 + +## 環境構築:バージョン管理ツールの導入 + +システム(OS)に最初から入っているRubyを直接使うのではなく、**バージョン管理ツール**(`rbenv`や`RVM`)を導入することを強く推奨します。 + +プロジェクトごとに異なるRubyバージョン(例: 2.7.x と 3.3.x)を簡単に切り替えることができ、システムをクリーンに保てます。 + +ここでは`rbenv`を使った一般的な流れを紹介します。(詳細なインストール手順はOSによって異なりますので、`rbenv`のGitHubリポジトリなどを参照してください。) + +1. **rbenv と ruby-build のインストール**: + Homebrew (macOS) や apt/yum (Linux) など、お使いのパッケージマネージャでインストールします。 + +2. **Rubyのインストール**: + + ```bash + # インストール可能なバージョン一覧を確認 + $ rbenv install -l + + # 最新の安定版(例: 3.3.0)をインストール + $ rbenv install 3.3.0 + ``` + +3. **使用するバージョンの設定**: + + ```bash + # このPCでのデフォルトバージョンとして設定 + $ rbenv global 3.3.0 + + # バージョンが切り替わったか確認 + $ ruby -v + ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] + ``` + +## 対話型シェル(irb)の活用 + +Rubyのインストールが完了したら、`irb` (Interactive Ruby) を起動してみましょう。これはRubyのREPL (Read-Eval-Print Loop) で、コード片を試したり、ドキュメント代わりに使ったりするのに非常に便利です。 + +ターミナルで`irb`と入力することで起動できます。 + +このウェブサイトではドキュメント内にRubyの実行環境を埋め込んでいます。 +以下のように青枠で囲われたコード例には自由にRubyコードを書いて試すことができます。 + +```ruby-repl:2 +irb(main):001> 10 * (5 + 3) +=> 80 +irb(main):002> "Ruby".length +=> 4 +irb(main):003> 3.times { puts "Welcome!" } +Welcome! +Welcome! +Welcome! +=> 3 +``` + +`=>` の右側に表示されているのが、式の**評価結果(返り値)**です。 + +`3.times`の例に注目してください。`puts "Welcome!"`が3回実行(出力)されていますが、`=> 3`と表示されています。これは、`3.times`というメソッド自体の返り値が`3`(レシーバである数値)であることを示しています。Rubyではすべての式が値を返すことを覚えておいてください。 + +## "Hello, World\!" とスクリプトの実行 + +最後に、定番の "Hello, World\!" を2通りの方法で実行してみましょう。 + +### irbでの実行 + +`puts`("put string")は、引数を標準出力(ターミナル)に出力し、最後に改行を追加するメソッドです。 + +```ruby-repl:3 +irb(main):001> puts "Hello, World!" +Hello, World! +=> nil +``` + +(`puts`メソッド自体の返り値は、常に`nil`です。) + +### スクリプトファイルでの実行 + +エディタで`hello.rb`という名前のファイルを作成します。 + +```ruby:hello.rb +#!/usr/bin/env ruby +# 1行目はShebang(シーバン)と言い、Unix系OSでスクリプトとして直接実行する際に使われます。 + +# 変数に文字列を代入 +message = "Hello from script file!" + +# 変数の内容を出力 +puts message +``` + +このファイルを実行するには、ターミナルで`ruby`コマンドの引数にファイル名を渡します。 + +このウェブサイト上では以下のように実行ボタンをクリックするとスクリプトの実行結果が表示されます。上の hello.rb のコードを変更して再度実行すると結果も変わるはずです。試してみてください。 + +```ruby-exec:hello.rb +Hello from script file! +``` + +おめでとうございます! これでRubyの世界への第一歩を踏み出しました。 +次の章では、Rubyの基本的な構文、データ型、そして他の言語にはない特徴的な「シンボル」について詳しく学んでいきます。 diff --git a/public/docs/ruby-10.md b/public/docs/ruby-10.md new file mode 100644 index 0000000..d7a994c --- /dev/null +++ b/public/docs/ruby-10.md @@ -0,0 +1,292 @@ +# 第10章: 標準ライブラリの活用 + +Rubyの強力な点の一つは、多くの一般的なタスクを処理するための豊富な「標準ライブラリ」が同梱されていることです。これらは "batteries included"(電池付属)とよく表現されます。 + +他の言語で `import` や `include` を使うのと同様に、Rubyでは `require` を使ってこれらのライブラリをロードします。ただし、`File` や `Time`、`Regexp` のようなコア機能の多くは、`require` なしで利用可能です。 + +この章では、特に使用頻度の高い標準ライブラリの機能を見ていきます。 + +## ファイル操作 (File, Dir, Pathname) + +ファイルシステムとのやり取りは、多くのアプリケーションで不可欠です。 + +### Fileクラスによる読み書き + +`File` クラスは、ファイルに対する基本的な読み書き操作を提供します。 + +**ファイルの書き込みと追記:** + +```ruby:file_io_example.rb +# 1. ファイルへの書き込み (上書き) +# シンプルな方法は File.write です +File.write('sample.txt', "Hello, Ruby Standard Library!\n") + +# 2. ファイルへの追記 +# mode: 'a' (append) オプションを指定します +File.write('sample.txt', "This is a second line.\n", mode: 'a') + +puts "File 'sample.txt' created and updated." +``` + +```ruby-exec:file_io_example.rb +File 'sample.txt' created and updated. +``` + +```text-readonly:sample.txt +``` + +**ファイルの読み込み:** + +```ruby:file_read_example.rb +# 'sample.txt' が存在すると仮定 + +# 1. ファイル全体を一度に読み込む +content = File.read('sample.txt') +puts "--- Reading all at once ---" +puts content + +# 2. 1行ずつ処理する (大きなファイルに効率的) +puts "\n--- Reading line by line ---" +File.foreach('sample.txt') do |line| + print "Line: #{line}" +end + +# 3. 処理後にファイルをクリーンアップ +File.delete('sample.txt') +puts "\n\nFile 'sample.txt' deleted." +``` + +```ruby-exec:file_read_example.rb +--- Reading all at once --- +Hello, Ruby Standard Library! +This is a second line. + +--- Reading line by line --- +Line: Hello, Ruby Standard Library! +Line: This is a second line. + +File 'sample.txt' deleted. +``` + +### DirクラスとPathname + +`Dir` クラスはディレクトリの内容を操作するために使われます。特に `Dir.glob` はワイルドカードを使ってファイルやディレクトリを検索するのに便利です。 + +しかし、パスの連結や解析を文字列として扱うのは面倒です。`Pathname` ライブラリは、パスをオブジェクトとして扱うための優れたインターフェースを提供します。 + +```ruby-repl:1 +irb(main):001:0> # Dir.glob はワイルドカードでファイルリストを取得できます +irb(main):002:0> Dir.glob('*.rb') # (irbを実行しているディレクトリによります) +=> [] + +irb(main):003:0> # Pathname を使うには require が必要 +irb(main):004:0> require 'pathname' +=> true + +irb(main):005:0> # 文字列の代わりに Pathname オブジェクトを作成 +irb(main):006:0> base_path = Pathname.new('/usr/local') +=> # + +irb(main):007:0> # + や / 演算子で安全にパスを連結できます +irb(main):008:0> lib_path = base_path + 'lib' +=> # +irb(main):009:0> bin_path = base_path / 'bin' +=> # + +irb(main):010:0> # パスの解析 +irb(main):011:0> file_path = Pathname.new('/var/log/app.log') +=> # +irb(main):012:0> file_path.basename # ファイル名 +=> # +irb(main):013:0> file_path.extname # 拡張子 +=> ".log" +irb(main):014:0> file_path.dirname # 親ディレクトリ +=> # +irb(main):015:0> file_path.absolute? # 絶対パスか? +=> true +``` + +## 日付と時刻 (Time, Date) + +Rubyには `Time`(組み込み)と `Date`(要 `require`)の2つの主要な日時クラスがあります。 + + * **Time:** 時刻(タイムスタンプ)をナノ秒までの精度で扱います。 + * **Date:** 日付(年月日)のみを扱い、カレンダー計算に特化しています。 + +```ruby-repl:2 +irb(main):001:0> # Time (組み込み) +irb(main):002:0> now = Time.now +=> 2025-11-04 11:32:00 +0900 (JST) +irb(main):003:0> now.year +=> 2025 +irb(main):004:0> now.monday? +=> false +irb(main):005:0> now.to_i # UNIXタイムスタンプ +=> 1762309920 + +irb(main):006:0> # strftime (string format time) でフォーマット +irb(main):007:0> now.strftime("%Y-%m-%d %H:%M:%S") +=> "2025-11-04 11:32:00" + +irb(main):008:0> # Date (require が必要) +irb(main):009:0> require 'date' +=> true +irb(main):010:0> today = Date.today +=> # +irb(main):011:0> today.strftime("%A") # 曜日 +=> "Tuesday" + +irb(main):012:0> # 文字列からのパース +irb(main):013:0> christmas = Date.parse("2025-12-25") +=> # +irb(main):014:0> (christmas - today).to_i # あと何日? +=> 51 +``` + +## JSONのパースと生成 (json) + +現代のWeb開発においてJSONの扱いは不可欠です。`json` ライブラリは、JSON文字列とRubyのHash/Arrayを相互に変換する機能を提供します。 + +```ruby-repl:3 +irb(main):001:0> require 'json' +=> true + +irb(main):002:0> # 1. JSON文字列 -> Rubyオブジェクト (Hash) へのパース +irb(main):003:0> json_data = '{"user_id": 123, "name": "Alice", "tags": ["admin", "ruby"]}' +=> "{\"user_id\": 123, \"name\": \"Alice\", \"tags\": [\"admin\", \"ruby\"]}" + +irb(main):004:0> parsed_data = JSON.parse(json_data) +=> {"user_id"=>123, "name"=>"Alice", "tags"=>["admin", "ruby"]} +irb(main):005:0> parsed_data['name'] +=> "Alice" +irb(main):006:0> parsed_data['tags'] +=> ["admin", "ruby"] + +irb(main):007:0> # 2. Rubyオブジェクト (Hash) -> JSON文字列 への生成 +irb(main):008:0> ruby_hash = { +irb(main):009:1* status: "ok", +irb(main):010:1* data: { item_id: 987, price: 1500 } +irb(main):011:1* } +=> {:status=>"ok", :data=>{:item_id=>987, :price=>1500}} + +irb(main):012:0> # .to_json メソッドが便利です +irb(main):013:0> json_output = ruby_hash.to_json +=> "{\"status\":\"ok\",\"data\":{\"item_id\":987,\"price\":1500}}" + +irb(main):014:0> # 人が読みやすいように整形 (pretty generate) +irb(main):015:0> puts JSON.pretty_generate(ruby_hash) +{ + "status": "ok", + "data": { + "item_id": 987, + "price": 1500 + } +} +=> nil +``` + +## 正規表現 (Regexp) と match + +Rubyの正規表現 (Regexp) は、Perl互換の強力なパターンマッチング機能を提供します。`/pattern/` リテラルで記述するのが一般的です。 + +### マッチの確認 (`=~` と `match`) + + * `=~` 演算子: マッチした位置のインデックス(0から始まる)を返すか、マッチしなければ `nil` を返します。 + * `String#match`: `MatchData` オブジェクトを返すか、マッチしなければ `nil` を返します。`MatchData` は、キャプチャグループ(`()`で囲んだ部分)へのアクセスに便利です。 + +```ruby-repl:4 +irb(main):001:0> text = "User: alice@example.com (Alice Smith)" +=> "User: alice@example.com (Alice Smith)" + +irb(main):002:0> # =~ は位置を返す +irb(main):003:0> text =~ /alice/ +=> 6 +irb(main):004:0> text =~ /bob/ +=> nil + +irb(main):005:0> # String#match は MatchData を返す +irb(main):006:0> # パターン: (ユーザー名)@(ドメイン) +irb(main):007:0> match_data = text.match(/(\w+)@([\w\.]+)/) +=> # + +irb(main):008:0> # マッチしたオブジェクトからキャプチャを取得 +irb(main):009:0> match_data[0] # マッチ全体 +=> "alice@example.com" +irb(main):010:0> match_data[1] # 1番目の () +=> "alice" +irb(main):011:0> match_data[2] # 2番目の () +=> "example.com" +``` + +### 検索と置換 (`scan` と `gsub`) + + * `String#scan`: マッチするすべての部分文字列を(キャプチャグループがあればその配列として)返します。 + * `String#gsub`: マッチするすべての部分を置換します (Global SUBstitute)。 + +```ruby-repl:5 +irb(main):001:0> log = "ERROR: code 500. WARNING: code 404. ERROR: code 403." +=> "ERROR: code 500. WARNING: code 404. ERROR: code 403." + +irb(main):002:0> # scan: 'ERROR: code (数字)' にマッチする部分をすべて探す +irb(main):003:0> log.scan(/ERROR: code (\d+)/) +=> [["500"], ["403"]] + +irb(main):004:0> # gsub: 'ERROR' を 'CRITICAL' に置換する +irb(main):005:0> log.gsub("ERROR", "CRITICAL") +=> "CRITICAL: code 500. WARNING: code 404. CRITICAL: code 403." + +irb(main):006:0> # gsub はブロックと正規表現を組み合わせて高度な置換が可能 +irb(main):007:0> # 数字(コード)を [] で囲む +irb(main):008:0> log.gsub(/code (\d+)/) do |match| +irb(main):009:1* # $1 は最後のマッチの1番目のキャプチャグループ +irb(main):010:1* "code [#{$1}]" +irb(main):011:1> end +=> "ERROR: code [500]. WARNING: code [404]. ERROR: code [403]." +``` + +## この章のまとめ + + * Rubyには、`require` でロードできる豊富な**標準ライブラリ**が付属しています。 + * **File**クラスはファイルの読み書きを、**Pathname**はパス操作をオブジェクト指向的に行います。 + * **Time**は時刻を、**Date**は日付を扱います。`strftime` でフォーマットできます。 + * **json**ライブラリは `JSON.parse`(文字列→Hash)と `to_json`(Hash→文字列)を提供します。 + * **Regexp**(`/pattern/`)はパターンマッチングに使います。`String#match` で `MatchData` を取得し、`scan` や `gsub` で検索・置換を行います。 + +これらは標準ライブラリのごく一部です。他にもCSVの処理 (`csv`)、HTTP通信 (`net/http`)、テスト (`minitest`) など、多くの機能が提供されています。 + +### 練習問題1: JSON設定ファイルの読み書き + +1. `config.json` ファイルを読み込み、内容をJSONパースしてRubyのHashに変換してください。 +2. そのHashの `logging` の値を `true` に変更し、さらに `:updated_at` というキーで現在時刻(文字列)を追加してください。 +3. 変更後のHashをJSON文字列に変換し、`config_updated.json` という名前でファイルに保存してください。(読みやすさのために `JSON.pretty_generate` を使っても構いません) + +```json-readonly:config.json +{"app_name": "RubyApp", "version": "1.0", "logging": false} +``` + +```ruby:practice10_1.rb +``` + +```ruby-exec:practice10_1.rb +``` + +```json-readonly:config_updated.json +``` + +### 練習問題2: ログファイルからの情報抽出 + +1. `system.log` というファイルを1行ずつ読み込みます。 +2. 正規表現を使い、`[INFO]` で始まり、かつ `logged in` という文字列を含む行だけを検出してください。 +3. マッチした行から、IPアドレス(`192.168.1.10` のような形式)を正規表現のキャプチャグループを使って抽出し、IPアドレスだけをコンソールに出力してください。 + +```text-readonly:system.log +[INFO] 2025-11-04 User 'admin' logged in from 192.168.1.10 +[WARN] 2025-11-04 Failed login attempt for user 'guest' +[INFO] 2025-11-04 Service 'payment_gateway' started. +``` + +```ruby:practice10_2.rb +``` + +```ruby-exec:practice10_2.rb +``` diff --git a/public/docs/ruby-11.md b/public/docs/ruby-11.md new file mode 100644 index 0000000..4637bd6 --- /dev/null +++ b/public/docs/ruby-11.md @@ -0,0 +1,358 @@ +# 第11章: テスト文化入門 - Minitest + +Rubyは動的型付け言語であり、コンパイル時ではなく実行時に型が決まります。これは柔軟で高速な開発を可能にする反面、型の不一致などによる単純なミスが実行時まで検出されにくいという特性も持ちます。 + +そのため、Rubyコミュニティでは「**テストは文化**」と言われるほど、自動化されたテストを書くことが重視されます。テストは、コードが期待通りに動作することを保証するだけでなく、未来の自分や他の開発者がコードをリファクタリング(修正・改善)する際の「安全網」として機能します。 + +この章では、Rubyに標準で添付されているテスティングフレームワーク「Minitest」を使い、テストの基本的な書き方と文化を学びます。 + +## 標準添付のテスティングフレームワーク「Minitest」 + +Minitestは、Rubyに標準で含まれている(=別途インストール不要)軽量かつ高速なテストフレームワークです。 + +Ruby on Railsなどの主要なフレームワークもデフォルトでMinitestを採用しており、Rubyのエコシステムで広く使われています。(RSpecという、よりDSL(ドメイン固有言語)ライクに記述できる人気のフレームワークもありますが、まずは標準のMinitestを理解することが基本となります。) + +Minitestは、`Minitest::Test` を継承する「Unitスタイル」と、`describe` ブロックを使う「Specスタイル」の2種類の書き方を提供しますが、この章では最も基本的なUnitスタイルを学びます。 + +## テストファイルの作成と実行 + +早速、簡単なクラスをテストしてみましょう。 + +### 1\. テスト対象のクラスの作成 + +まず、テスト対象となる簡単な電卓クラスを作成します。 + +```ruby:calculator.rb +# シンプルな電卓クラス +class Calculator + def add(a, b) + a + b + end + + def subtract(a, b) + a - b + end +end +``` + +### 2\. テストファイルの作成 + +Rubyの規約では、テストファイルは `test_` プレフィックス(例: `test_calculator.rb`)または `_test.rb` サフィックス(例: `calculator_test.rb`)で作成するのが一般的です。ここでは `test_calculator.rb` を作成します。 + +テストファイルは、以下の要素で構成されます。 + +1. `require 'minitest/autorun'` + * Minitestライブラリを読み込み、ファイル実行時にテストが自動で走るようにします。 +2. `require_relative 'ファイル名'` + * テスト対象のファイル(今回は `calculator.rb`)を読み込みます。 +3. `class クラス名 < Minitest::Test` + * テストクラスを定義し、`Minitest::Test` を継承します。 +4. `def test_メソッド名` + * `test_` で始まるメソッドを定義します。これが個々のテストケースとなります。 + +```ruby:test_calculator.rb +require 'minitest/autorun' +require_relative 'calculator' + +class CalculatorTest < Minitest::Test + # `test_` で始まるメソッドがテストとして実行される + def test_addition + # テスト対象のインスタンスを作成 + calc = Calculator.new + + # 期待値 (Expected) + expected = 5 + # 実際の結果 (Actual) + actual = calc.add(2, 3) + + # アサーション(後述) + # 期待値と実際の結果が等しいことを検証する + assert_equal(expected, actual) + end + + def test_subtraction + calc = Calculator.new + # アサーションは1行で書くことが多い + assert_equal(1, calc.subtract(4, 3)) + end +end +``` + +### 3\. テストの実行 + +ターミナルで、作成した**テストファイル**を実行します。 + +```ruby-exec:test_calculator.rb +Run options: --seed 51740 + +# Running: + +.. + +Finished in 0.001099s, 1819.8362 runs/s, 1819.8362 assertions/s. + +2 runs, 2 assertions, 0 failures, 0 errors, 0 skips +``` + +実行結果のサマリに注目してください。 + + * `.`(ドット): テストが成功(Pass)したことを示します。 + * `2 runs, 2 assertions`: 2つのテスト(`test_addition` と `test_subtraction`)が実行され、合計2回のアサーション(`assert_equal`)が成功したことを意味します。 + * `0 failures, 0 errors`: 失敗もエラーもありません。 + +もしテストが失敗すると、`F`(Failure)や `E`(Error)が表示され、詳細なレポートが出力されます。 + +## アサーション(assert\_equal, assert 等)の書き方 + +アサーション(Assertion = 表明、断言)は、「この値はこうあるべきだ」と検証するためのメソッドです。Minitestは `Minitest::Test` を継承したクラス内で、様々なアサーションメソッドを提供します。 + +### `assert_equal(expected, actual)` + +最もよく使うアサーションです。「期待値(expected)」と「実際の結果(actual)」が `==` で等しいことを検証します。 + +> **⚠️ 注意:** 引数の順序が重要です。\*\*1番目が「期待値」、2番目が「実際の結果」\*\*です。逆にすると、失敗時のメッセージが非常に分かりにくくなります。 + +```ruby-repl +irb> require 'minitest/assertions' +=> true +irb> include Minitest::Assertions +=> Object +irb> def assert_equal(expected, actual); super; end # irbで使うための設定 +=> :assert_equal + +irb> assert_equal 5, 2 + 3 +=> true + +irb> assert_equal 10, 2 + 3 +# Minitest::Assertion: <--- 失敗(Assertion Failed) +# Expected: 10 +# Actual: 5 +``` + +### `assert(test)` + +`test` が **true**(またはtrueと評価される値)であることを検証します。偽(`false` または `nil`)の場合は失敗します。 + +```ruby-repl +irb> assert "hello".include?("e") +=> true +irb> assert [1, 2, 3].empty? +# Minitest::Assertion: Expected [] to be empty?. +``` + +### `refute(test)` + +`assert` の逆です。`test` が **false** または `nil` であることを検証します。 + +```ruby-repl +irb> refute [1, 2, 3].empty? +=> true +irb> refute "hello".include?("e") +# Minitest::Assertion: Expected "hello".include?("e") to be falsy. +``` + +### `assert_nil(obj)` + +`obj` が `nil` であることを検証します。 + +```ruby-repl +irb> a = nil +=> nil +irb> assert_nil a +=> true +``` + +### `assert_raises(Exception) { ... }` + +ブロック `{ ... }` を実行した結果、指定した例外(`Exception`)が発生することを検証します。 + +これは、意図したエラー処理が正しく動作するかをテストするのに非常に重要です。 + +```ruby:test_calculator_errors.rb +require 'minitest/autorun' + +class Calculator + def divide(a, b) + raise ZeroDivisionError, "Cannot divide by zero" if b == 0 + a / b + end +end + +class CalculatorErrorTest < Minitest::Test + def test_division_by_zero + calc = Calculator.new + + # ブロック内で ZeroDivisionError が発生することを期待する + assert_raises(ZeroDivisionError) do + calc.divide(10, 0) + end + end +end +``` + +```ruby-exec:test_calculator_errors.rb +Run options: --seed 19800 + +# Running: + +. + +Finished in 0.000624s, 1602.5641 runs/s, 1602.5641 assertions/s. + +1 runs, 1 assertions, 0 failures, 0 errors, 0 skips +``` + +## 簡単なTDD(テスト駆動開発)の体験 + +TDD (Test-Driven Development) は、機能のコードを書く前に、**まず失敗するテストコードを書く**開発手法です。TDDは以下の短いサイクルを繰り返します。 + +1. **Red (レッド):** + * これから実装したい機能に対する「失敗するテスト」を書きます。 + * まだ機能が存在しないため、テストは(当然)失敗(Red)します。 +2. **Green (グリーン):** + * そのテストをパスさせるための**最小限の**機能コードを実装します。 + * テストが成功(Green)すればOKです。コードの綺麗さはまだ問いません。 +3. **Refactor (リファクタリング):** + * テストが成功した状態を維持したまま、コードの重複をなくしたり、可読性を上げたりする「リファクタリング」を行います。 + +`Calculator` クラスに、`multiply`(掛け算)メソッドをTDDで追加してみましょう。 + +### 1\. Red: 失敗するテストを書く + +まず、`test_calculator.rb` に `multiply` のテストを追加します。 + +```ruby:calculator.rb +# シンプルな電卓クラス +class Calculator + def add(a, b) + a + b + end + + def subtract(a, b) + a - b + end +end +``` + +```ruby:test_calculator_tdd.rb +require 'minitest/autorun' +require_relative 'calculator' # calculator.rb は add と subtract のみ + +class CalculatorTest < Minitest::Test + def setup + # @calc をインスタンス変数にすると、各テストメソッドで使える + @calc = Calculator.new + end + + def test_addition + assert_equal(5, @calc.add(2, 3)) + end + + def test_subtraction + assert_equal(1, @calc.subtract(4, 3)) + end + + # --- TDDサイクル スタート --- + + # 1. Red: まずテストを書く + def test_multiplication + assert_equal(12, @calc.multiply(3, 4)) + end +end +``` + +この時点で `calculator.rb` に `multiply` メソッドは存在しません。テストを実行します。 + +```ruby-exec:test_calculator_tdd.rb +# (実行結果の抜粋) +... +Error: +CalculatorTest#test_multiplication: +NoMethodError: undefined method `multiply' for # +... +1 runs, 0 assertions, 0 failures, 1 errors, 0 skips +``` + +期待通り、`NoMethodError` でテストが**エラー (E)** になりました。これが「Red」の状態です。(Failure (F) はアサーションが期待と違った場合、Error (E) はコード実行中に例外が発生した場合を指します) + +### 2\. Green: テストを通す最小限のコードを書く + +次に、`calculator.rb` に以下のように `multiply` メソッドを実装し、テストをパス(Green)させます。 + +```ruby +class Calculator + def add(a, b) + a + b + end + + def subtract(a, b) + a - b + end + + # 2. Green: テストを通す最小限の実装 + def multiply(a, b) + a * b + end +end +``` + +`calculator.rb` を編集し、再びテストを実行すると、以下のようにすべてのテストが成功します。「Green」の状態です。 + +```bash +$ ruby test_calculator_tdd.rb +... +Finished in ... +3 runs, 3 assertions, 0 failures, 0 errors, 0 skips +``` + +### 3\. Refactor: リファクタリング + +今回は実装が非常にシンプルなのでリファクタリングの必要はあまりありませんが、もし `multiply` の実装が複雑になったり、他のメソッドとコードが重複したりした場合は、この「Green」の(テストが成功している)状態で安心してコードをクリーンアップします。 + +TDDは、この「Red -\> Green -\> Refactor」のサイクルを高速で回すことにより、バグの少ない、メンテンスしやすいコードを堅実に構築していく手法です。 + +## 📈 この章のまとめ + + * Rubyは動的型付け言語であるため、実行時の動作を保証する**テストが非常に重要**です。 + * **Minitest** はRubyに標準添付された軽量なテスティングフレームワークです。 + * テストファイルは `require 'minitest/autorun'` し、`Minitest::Test` を継承します。 + * テストメソッドは `test_` プレフィックスで定義します。 + * `assert_equal(期待値, 実際の結果)` が最も基本的なアサーションです。 + * `assert` (true検証), `refute` (false検証), `assert_raises` (例外検証) などもよく使われます。 + * **TDD (テスト駆動開発)** は「Red (失敗) -\> Green (成功) -\> Refactor (改善)」のサイクルで開発を進める手法です。 + +### 練習問題1: Stringクラスのテスト + +`Minitest::Test` を使って、Rubyの組み込みクラスである `String` の動作をテストする `test_string.rb` を作成してください。以下の2つのテストメソッドを実装してください。 + + * `test_string_length`: `"hello"` の `length` が `5` であることを `assert_equal` で検証してください。 + * `test_string_uppercase`: `"world"` を `upcase` した結果が `"WORLD"` であることを `assert_equal` で検証してください。 + +```ruby:test_string.rb +require 'minitest/autorun' + + +``` + +```ruby-exec:test_string.rb +``` + +### 練習問題2: TDDでUserクラスを実装 + +TDDの「Red -\> Green」サイクルを体験してください。 + +1. (Red)`User` クラスに `first_name` と `last_name` を渡してインスタンス化し、`full_name` メソッドを呼ぶと `"First Last"` のように連結された文字列が返ることを期待するテスト `test_full_name` を含む `test_user.rb` を先に作成してください。(この時点では `user.rb` は空か、存在しなくても構いません) +2. (Green)テストがパスするように、`user.rb` に `User` クラスを実装してください。(`initialize` で名前を受け取り、`full_name` メソッドで連結します) + + +```ruby:user.rb +``` + +```ruby:test_user.rb +require 'minitest/autorun' +require_relative 'user' + +``` + +```ruby-exec:test_user.rb +``` diff --git a/public/docs/ruby-12.md b/public/docs/ruby-12.md new file mode 100644 index 0000000..3c9004a --- /dev/null +++ b/public/docs/ruby-12.md @@ -0,0 +1,212 @@ +# 第12章: メタプログラミング入門 + +Rubyの最も強力であり、同時に最も特徴的な側面の一つが「メタプログラミング」です。これは「コードがコードを書く(あるいは変更する)」能力を指します。他の言語でコンパイル時やリフレクションAPIを通じて行っていた操作の多くを、Rubyでは実行時に直接、かつ柔軟に行うことができます。 + +## Rubyの動的な性質 + +Rubyは非常に動的な言語です。クラスは実行中に変更可能であり、メソッドの追加や削除、上書きがいつでも行えます。この章では、その動的な性質を利用したメタプログラミングの基本的な手法を学びます。 + + * **オープンクラス**: Rubyでは、既存のクラス(組み込みクラスさえも)を後から「開いて」メソッドを追加・変更できます。 + * **実行時**: 多くの決定がコンパイル時ではなく実行時に行われます。 + +これらの性質が、DRY (Don't Repeat Yourself) の原則を追求し、柔軟なDSL(ドメイン固有言語)を構築するための基盤となります。 + +## send: メソッドを動的に呼び出す + +通常、メソッドは `object.method_name` のようにドット(`.`)を使って呼び出します。しかし、呼び出したいメソッド名が実行時までわからない場合、`send` メソッド(または `public_send`)が役立ちます。 + +`send` は、第1引数にメソッド名を**シンボル**(`:`)または**文字列**で受け取り、残りの引数をそのメソッドに渡して実行します。 + +```ruby-repl +irb(main):001> "hello".send(:upcase) +=> "HELLO" +irb(main):002> "hello".send("length") +=> 5 +irb(main):003> 10.send(:+, 5) # 演算子も内部的にはメソッドです +=> 15 +irb(main):004> +irb(main):004> method_to_call = :reverse +irb(main):005> "Ruby".send(method_to_call) +=> "ybuR" +``` + +> **注意**: `send` は `private` メソッドも呼び出すことができます。意図せず `private` メソッドを呼び出さないように、通常は `public_send` を使う方が安全です。 + +## define\_method: メソッドを動的に定義する + +メソッドを動的に(実行時に)定義したい場合、`define_method` を使用します。これは主にクラスやモジュールの定義内で使われます。 + +`define_method` は、第1引数に定義したいメソッド名(シンボル)を、第2引数にブロック(ProcやLambda)を取ります。このブロックが、新しく定義されるメソッドの本体となります。 + +例えば、似たようなメソッドを多数定義する必要がある場合に非常に便利です。 + +```ruby:dynamic_greeter.rb +class DynamicGreeter + # 定義したい挨拶のリスト + GREETINGS = { + hello: "Hello", + goodbye: "Goodbye", + hi: "Hi" + } + + GREETINGS.each do |name, prefix| + # define_methodを使ってメソッドを動的に定義する + define_method(name) do |target| + puts "#{prefix}, #{target}!" + end + end +end + +greeter = DynamicGreeter.new + +# 動的に定義されたメソッドを呼び出す +greeter.hello("World") +greeter.goodbye("Alice") +greeter.hi("Bob") +``` + +```ruby-exec:dynamic_greeter.rb +Hello, World! +Goodbye, Alice! +Hi, Bob! +``` + +## method\_missing: 存在しないメソッドへの応答 + +オブジェクトに対して定義されていないメソッドが呼び出されると、Rubyは例外(`NoMethodError`)を発生させる前に、`method_missing` という特別なメソッドを呼び出そうと試みます。 + +この `method_missing` を自分でオーバーライドすることで、定義されていないメソッド呼び出しを「キャッチ」し、動的に処理できます。 + +`method_missing` は以下の引数を受け取ります。 + +1. 呼び出されようとしたメソッド名(シンボル) +2. そのメソッドに渡された引数(配列) +3. そのメソッドに渡されたブロック(存在する場合) + +```ruby:ghost_methods.rb +class DynamicLogger + def method_missing(method_name, *args, &block) + # 呼び出されたメソッド名が 'log_' で始まるかチェック + if method_name.to_s.start_with?("log_") + # 'log_' の部分を取り除いてレベル名とする + level = method_name.to_s.delete_prefix("log_") + + # メッセージ(引数)を取得 + message = args.first || "(no message)" + + puts "[#{level.upcase}] #{message}" + else + # 関係ないメソッド呼び出しは、通常通り NoMethodError を発生させる + super + end + end + + # respond_to? が正しく動作するように、respond_to_missing? も定義するのがベストプラクティス + def respond_to_missing?(method_name, include_private = false) + method_name.to_s.start_with?("log_") || super + end +end + +logger = DynamicLogger.new + +logger.log_info("Application started.") +logger.log_warning("Cache is empty.") +logger.log_error("File not found.") + +# respond_to? の動作確認 +puts "Responds to log_info? #{logger.respond_to?(:log_info)}" +puts "Responds to undefined_method? #{logger.respond_to?(:undefined_method)}" + +# 存在しないメソッド(super呼び出し) +# logger.undefined_method # => NoMethodError +``` + +```ruby-exec:ghost_methods.rb +[INFO] Application started. +[WARNING] Cache is empty. +[ERROR] File not found. +Responds to log_info? true +Responds to undefined_method? false +``` + +## Railsなどでの活用例 + +Rubyのメタプログラミングは、Ruby on Railsのようなフレームワークで広く活用されています。これにより、開発者は定型的なコード(ボイラープレート)を大量に書く必要がなくなり、宣言的な記述が可能になります。 + + * **Active Record (ORM)**: + * `method_missing` の典型的な例です。`User.find_by_email("test@example.com")` のようなメソッドは、`User` クラスに明示的に定義されていません。Active Recordは `method_missing` を使って `find_by_` プレフィックスを検出し、`email` カラムで検索するSQLを動的に生成します。 + * **関連付け (Associations)**: + * `has_many :posts` や `belongs_to :user` といった記述。これらは単なる宣言に見えますが、内部では `define_method` を使い、`user.posts` や `post.user` といった便利なメソッドを実行時に定義しています。 + +このように、メタプログラミングはRubyエコシステムの「魔法」の多くを支える技術であり、フレームワークの内部を理解する上で不可欠です。 + +## ⚡ この章のまとめ + + * **メタプログラミング**とは、コードが実行時に自身の構造(クラスやメソッド)を操作する技術です。 + * `send`(または `public_send`)は、メソッド名を文字列やシンボルで指定し、動的にメソッドを呼び出します。 + * `define_method` は、実行時にメソッドを動的に定義します。DRYを保つのに役立ちます。 + * `method_missing` は、定義されていないメソッド呼び出しを捕捉し、柔軟なインターフェース(DSL)を構築するために使われます。 + * メタプログラミングは非常に強力ですが、コードの可読性やデバッグの難易度を上げる可能性もあります。使い所を理解し、**乱用は避ける**ことが重要です。 + +## 練習問題1: 動的アクセサ + +`define_method` を使って、指定された属性名の配列からゲッター(`attr_reader`)とセッター(`attr_writer`)を動的に定義するメソッド `my_attr_accessor` を持つモジュールを作成してください。(ヒント: インスタンス変数 `@name` を読み書きするメソッドを定義します) + +```ruby:practice12_1.rb +module DynamicAccessor + def my_attr_accessor(*attrs) + attrs.each do |attr| + # ゲッターとセッターを動的に定義するコードをここに書く + + + end + end +end + +class Person + extend DynamicAccessor + + my_attr_accessor :name, :age +end +person = Person.new +person.name = "Alice" +person.age = 30 + +puts "Name: #{person.name}, Age: #{person.age}" +``` + +```ruby-exec:practice12_1.rb +Name: Alice, Age: 30 +``` + +### 練習問題2: シンプルな設定オブジェクト + +`method_missing` を使って、ハッシュのように動作する `SimpleConfig` クラスを作成してください。`config.api_key = "12345"` のように値を設定でき、`config.api_key` で値を取得できるようにしてください。設定されていないキーを呼び出した場合は `nil` を返すようにします。 + +```ruby:practice12_2.rb +class SimpleConfig + def initialize + @settings = {} + end + + def method_missing(method_name, *args, &block) + # ここにコードを書いてください + + + end + + def respond_to_missing?(method_name, include_private = false) + true + end +end + +config = SimpleConfig.new +config.api_key = "12345" +puts "API Key: #{config.api_key.inspect}" +puts "Timeout: #{config.timeout.inspect}" # 設定されていないキー +``` + +```ruby-exec:practice12_2.rb +API Key: "12345" +Timeout: nil +``` diff --git a/public/docs/ruby-2.md b/public/docs/ruby-2.md new file mode 100644 index 0000000..821662b --- /dev/null +++ b/public/docs/ruby-2.md @@ -0,0 +1,212 @@ +# 第2章: 基本構文とデータ型 - Rubyの「書き方」 + +Rubyへようこそ!他の言語の経験がある皆さんなら、Rubyの柔軟で読みやすい構文にすぐに慣れるでしょう。この章では、Rubyの基本的な構成要素を見ていきます。 + +## 💎 変数、定数、スコープ + +Rubyの変数は型宣言を必要としませんが、変数の「スコープ(可視範囲)」は名前の付け方によって決まります。これは他の言語と大きく異なる点です。 + + * **ローカル変数**: `my_var` + * 小文字または `_` で始まります。定義されたスコープ(メソッド定義、ブロック、ファイルのトップレベルなど)でのみ有効です。 + * **インスタンス変数**: `@my_var` + * `@` で始まります。特定のオブジェクトのインスタンスに属し、そのオブジェクトのメソッド内からアクセスできます。(クラスの章で詳述します) + * **クラス変数**: `@@my_var` + * `@@` で始まります。クラス全体とそのサブクラスで共有されます。(クラスの章で詳述します) + * **グローバル変数**: `$my_var` + * `$` で始まります。プログラムのどこからでもアクセス可能ですが、グローバルな状態を持つため、使用は最小限に抑えるべきです。 + * **定数**: `MY_CONSTANT` + * 大文字で始まります。一度定義すると変更すべきではない値を示します(技術的には変更可能ですが、Rubyが警告を出します)。 + +```ruby-repl:1 +irb(main):001> local_var = "I am local" +=> "I am local" +irb(main):002> @instance_var = "I belong to an object" +=> "I belong to an object" +irb(main):003> $global_var = "Available everywhere" +=> "Available everywhere" +irb(main):004> MY_CONSTANT = 3.14 +=> 3.14 +irb(main):005> MY_CONSTANT = 3.14159 # 警告が出ます +(irb):5: warning: already initialized constant MY_CONSTANT +(irb):4: warning: previous definition of MY_CONSTANT was here +=> 3.14159 +``` + +## 🔢 Rubyの基本データ型 + +Rubyには多くの組み込みデータ型がありますが、まずは基本的なものを押さえましょう。 + + * **Integer (整数)**: `1`, `100`, `-5`, `1_000_000` ( `_` は読みやすさのためのもので、無視されます) + * **Float (浮動小数点数)**: `1.5`, `3.14`, `-0.001` + * **String (文字列)**: `"Hello"`, `'World'` + * **Boolean (真偽値)**: `true`, `false` + * **NilClass (nil)**: `nil` (何も存在しないことを示す唯一の値) + * **Array (配列)**: `[1, "apple", true]` + * **Hash (ハッシュ)**: `{"key1" => "value1", :key2 => "value2"}` + * **Symbol (シンボル)**: `:my_symbol` (後述します) + +Rubyでは、これらすべてが「オブジェクト」であり、メソッドを持っています。 + +```ruby-repl:2 +irb(main):001> 100.class +=> Integer +irb(main):002> "Hello".class +=> String +irb(main):003> 3.14.class +=> Float +irb(main):004> true.class +=> TrueClass +irb(main):005> nil.class +=> NilClass +irb(main):006> [1, 2].class +=> Array +irb(main):007> {a: 1}.class +=> Hash +irb(main):008> :symbol.class +=> Symbol +``` + +## 🚫 重要: nil と false の扱い + +Rubyの条件分岐(`if`文など)において、**偽 (falsey) として扱われるのは `nil` と `false` の2つだけ**です。 + +これは非常に重要です。C言語やJavaScriptなどの `0`、空文字列 `""`、空配列 `[]` が偽として扱われる言語とは異なります。Rubyでは、これらはすべて**真 (truthy)** として扱われます。 + +```ruby:truthy_check.rb +def check_truthy(label, value) + if value + puts "[#{label}] は真 (truthy) です。" + else + puts "[#{label}] は偽 (falsey) です。" + end +end + +check_truthy("false", false) +check_truthy("nil", nil) +puts "---" +check_truthy("true", true) +check_truthy("0 (Integer)", 0) +check_truthy("1 (Integer)", 1) +check_truthy("空文字列 \"\"", "") +check_truthy("文字列 \"abc\"", "abc") +check_truthy("空配列 []", []) +check_truthy("空ハッシュ {}", {}) +``` + +```ruby-exec:truthy_check.rb +[false] は偽 (falsey) です。 +[nil] は偽 (falsey) です。 +--- +[true] は真 (truthy) です。 +[0 (Integer)] は真 (truthy) です。 +[1 (Integer)] は真 (truthy) です。 +[空文字列 ""] は真 (truthy) です。 +[文字列 "abc"] は真 (truthy) です。 +[空配列 []] は真 (truthy) です。 +[空ハッシュ {}] は真 (truthy) です。 +``` + +## 💬 重要: シンボル (Symbol) とは何か? + +シンボルは、他の言語の経験者にとってRubyの最初の「つまずきポイント」かもしれません。 + +シンボルはコロン ( `:` ) で始まります(例: `:name`, `:status`)。 + +**文字列 (String) とシンボル (Symbol) の違い:** + +1. **イミュータブル (Immutable)**: + * シンボルは一度作成されると変更できません。`"hello"[0] = "H"` は可能ですが、 `:hello` に対してこのような操作はできません。 +2. **一意性 (Identity)**: + * 同じ内容の文字列は、作成されるたびに異なるオブジェクトID(メモリ上の場所)を持つことがあります。 + * 同じ内容のシンボルは、プログラム全体で**常に同一のオブジェクト**を指します。 +3. **パフォーマンス**: + * シンボルは内部的に整数として扱われるため、文字列の比較よりも高速です。 + +**主な用途**: + + * **ハッシュのキー**: パフォーマンスとメモリ効率のため、シンボルはハッシュのキーとして非常によく使われます。 + * `user = { name: "Alice", age: 30 }` (これは `{ :name => "Alice", :age => 30 }` のシンタックスシュガーです) + * **メソッド名や状態の識別子**: `status = :pending`, `status = :completed` のように、固定された「名前」や「状態」を表すのに使われます。 + +```ruby-repl:3 +irb(main):001> "hello".object_id # 実行ごとに変わる +=> 60 +irb(main):002> "hello".object_id # 異なるID +=> 80 +irb(main):003> :hello.object_id # 常に同じ +=> 1084828 +irb(main):004> :hello.object_id # 同一のID +=> 1084828 +irb(main):005> "status".to_sym # 文字列とシンボルの変換 +=> :status +irb(main):006> :status.to_s +=> "status" +``` + +シンボルは「名前」そのもの、文字列は「データ」そのもの、と考えると分かりやすいかもしれません。 + +## 🚀 メソッド呼び出し(括弧の省略記法) + +Rubyでは、メソッドを呼び出す際の括弧 `()` を省略できます(ただし、曖昧さが生じない場合に限ります)。 + +```ruby-repl:4 +irb(main):001> puts("Hello, World!") # 括弧あり (推奨されることが多い) +Hello, World! +=> nil +irb(main):002> puts "Hello, World!" # 括弧なし (DSLや単純な呼び出しでよく使われる) +Hello, World! +=> nil +irb(main):003> 5.+(3) # '+' も実はメソッド呼び出し +=> 8 +irb(main):004> 5 + 3 # これは 5.+(3) のシンタックスシュガー +=> 8 +``` + +括弧を省略するとコードが読みやすくなる場合がありますが、メソッドチェーンが続く場合や、引数が複雑な場合は括弧を付けた方が明確です。 + +## 📜 文字列操作と式展開 + +Rubyの文字列は強力で、特に「式展開」は頻繁に使われます。 + + * **シングルクォート (`'...'`)**: ほぼそのまま文字列として扱います。`\n`(改行)などのエスケープシーケンスや式展開は解釈**されません**(`\'` と `\\` を除く)。 + * **ダブルクォート (`"..."`)**: エスケープシーケンス(`\n`, `\t` など)を解釈し、**式展開 (Interpolation)** を行います。 + +式展開は `#{...}` という構文を使い、`...` の部分でRubyのコードを実行し、その結果を文字列に埋め込みます。 + +```ruby-repl:5 +irb(main):001> name = "Alice" +=> "Alice" +irb(main):002> puts 'Hello, #{name}\nWelcome!' # シングルクォート +Hello, #{name}\nWelcome! +=> nil +irb(main):003> puts "Hello, #{name}\nWelcome!" # ダブルクォート +Hello, Alice +Welcome! +=> nil +irb(main):004> puts "1 + 2 = #{1 + 2}" # 式展開内では計算も可能 +1 + 2 = 3 +=> nil +irb(main):005> "Ruby" + " " + "Rocks" # 文字列の連結と繰り返し +=> "Ruby Rocks" +irb(main):006> "Go! " * 3 +=> "Go! Go! Go! " +``` + +## 📝 この章のまとめ + + * Rubyの変数は、先頭の記号 (`@`, `@@`, `$`) によってスコープが決まる。 + * `false` と `nil` のみが偽 (falsey) であり、`0` や `""` も真 (truthy) として扱われる。 + * シンボル (`:name`) はイミュータブルで一意な「名前」を表し、主にハッシュのキーや識別子として使われる。 + * メソッド呼び出しの括弧は、曖昧さがない限り省略できる。 + * ダブルクォート文字列 (`"..."`) は式展開 `#{...}` をサポートする。 + +### 練習問題1: 式展開とデータ型 + +ユーザーの名前(`name`)と年齢(`age`)を変数に代入してください。 +次に、`"#{...}"`(式展開)を使い、「(名前)さんの年齢は(年齢)歳です。5年後は(5年後の年齢)歳になります。」という文字列を出力するスクリプトを作成してください。 + +```ruby:practice2_1.rb +``` + +```ruby-exec:practice2_1.rb +``` diff --git a/public/docs/ruby-3.md b/public/docs/ruby-3.md new file mode 100644 index 0000000..9d1e31e --- /dev/null +++ b/public/docs/ruby-3.md @@ -0,0 +1,354 @@ +# 第3章: 制御構造とメソッド定義 + +Rubyの制御構造は、他の多くの言語と似ていますが、Rubyの「すべてが式である」という哲学と、読みやすさを重視した構文(`unless`など)に特徴があります。また、メソッド(関数)の定義は非常に柔軟で、強力な引数の扱いや例外処理の仕組みを備えています。 + +## 条件分岐 + +Rubyの条件分岐は、`if`、`unless`、`case`が基本です。`if`や`case`は文(Statement)ではなく**式(Expression)**であるため、それ自体が値を返します。 + +### if, else, elsif + +基本的な構文は他言語と同様ですが、`else if`は `elsif`(`e`が1つ)と綴る点に注意してください。 + +`if`は値を返すため、結果を変数に代入できます。 + +```ruby-repl:1 +irb(main):001:0> score = 85 +=> 85 +irb(main):002:0> grade = if score > 90 +irb(main):003:1* "A" +irb(main):004:1* elsif score > 80 # "else if" ではない +irb(main):005:1* "B" +irb(main):006:1* else +irb(main):007:1* "C" +irb(main):008:1* end +=> "B" +irb(main):009:0> puts "あなたの成績は#{grade}です" +あなたの成績はBです +=> nil +``` + +### unless + +`unless`は `if !`(もし~でなければ)の糖衣構文(Syntactic Sugar)です。条件が**偽 (false)** の場合にブロックが実行されます。 + +```ruby-repl:2 +irb(main):010:0> logged_in = false +=> false +irb(main):011:0> unless logged_in +irb(main):012:1* puts "ログインしてください" +irb(main):013:1* end +ログインしてください +=> nil +``` + +> **補足:** `unless` に `else` を付けることも可能ですが、多くの場合 `if` を使った方が可読性が高くなります。 + +### case + +C言語やJavaの `switch` 文に似ていますが、より強力です。`when` 節では、複数の値、範囲(Range)、正規表現、さらにはクラスを指定することもできます。`break` は不要です。 + +```ruby:case_example.rb +def analyze_input(input) + puts "Input: #{input.inspect}" + result = case input + when 0 + "ゼロ" + when 1..9 + "一桁の数字" + when "admin", "guest" + "特定のユーザー" + when String + "その他の文字列" + when /Error/ + "エラーメッセージ" + else + "不明な型" + end + puts "Result: #{result}" +end + +analyze_input(5) +analyze_input("guest") +analyze_input("Some value") +analyze_input(nil) +``` + +```ruby-exec:case_example.rb +Input: 5 +Result: 一桁の数字 +Input: "guest" +Result: 特定のユーザー +Input: "Some value" +Result: その他の文字列 +Input: nil +Result: 不明な型 +``` + +## 繰り返し処理 + +Rubyでは、後の章で学ぶイテレータ(`each`など)が繰り返し処理の主流ですが、C言語スタイルの `while` や `until` も利用可能です。 + +### while + +条件が**真 (true)** の間、ループを続けます。 + +```ruby-repl:3 +irb(main):001:0> i = 0 +=> 0 +irb(main):002:0> while i < 3 +irb(main):003:1* print i, " " # printは改行しません +irb(main):004:1* i += 1 # Rubyに i++ はありません +irb(main):005:1* end +0 1 2 => nil +``` + +### until + +`while !` と同じです。条件が**偽 (false)** の間、ループを続けます。 + +```ruby-repl:4 +irb(main):006:0> counter = 5 +=> 5 +irb(main):007:0> until counter == 0 +irb(main):008:1* print counter, " " +irb(main):009:1* counter -= 1 +irb(main):010:1* end +5 4 3 2 1 => nil +``` + +## メソッドの定義 (def) + +Rubyでは、`def` キーワードを使ってメソッドを定義します。 + +### 基本的な定義と戻り値(returnの省略) + +Rubyのメソッドは、**最後に評価された式の結果**を暗黙的に返します。`return` キーワードは、メソッドの途中で明示的に値を返したい場合(早期リターン)に使いますが、必須ではありません。 + +```ruby:method_return.rb +# 最後に評価された a + b が自動的に戻り値となる +def add(a, b) + a + b +end + +# 早期リターンで return を使う例 +def check_value(val) + if val < 0 + return "Negative" # ここで処理が中断 + end + + # val >= 0 の場合は、この式が評価され、戻り値となる + "Positive or Zero" +end + +puts add(10, 5) +puts check_value(-10) +puts check_value(10) +``` + +```ruby-exec:method_return.rb +15 +Negative +Positive or Zero +``` + +## 引数の種類 + +Rubyは、デフォルト引数、キーワード引数、可変長引数など、柔軟な引数の定義をサポートしています。 + +### デフォルト引数 + +引数にデフォルト値を設定できます。 + +```ruby-repl:5 +irb(main):001:0> def greet(name = "Guest") +irb(main):002:1* "Hello, #{name}!" +irb(main):003:1* end +=> :greet +irb(main):004:0> greet("Alice") +=> "Hello, Alice!" +irb(main):005:0> greet +=> "Hello, Guest!" +``` + +### キーワード引数 + +Pythonのように、引数名を指定して値を渡すことができます。`:`(コロン)を末尾に付けます。キーワード引数は可読性を大幅に向上させます。 + +```ruby:keyword_arguments.rb +# name: は必須のキーワード引数 +# age: はデフォルト値を持つキーワード引数 +def register_user(name:, age: nil, admin: false) + puts "User: #{name}" + puts "Age: #{age}" if age + puts "Admin: #{admin}" +end + +# 順序を問わない +register_user(admin: true, name: "Taro") + +puts "---" + +# 必須の name を省略すると ArgumentError になる +begin + register_user(age: 30) +rescue ArgumentError => e + puts e.message +end +``` + +```ruby-exec:keyword_arguments.rb +User: Taro +Admin: true +--- +missing keyword: :name +``` + +### 可変長引数 (Splat演算子) + +引数の先頭に `*`(Splat演算子)を付けると、任意の数の引数を配列として受け取ることができます。 + +```ruby-repl:6 +irb(main):006:0> def summarize(*items) +irb(main):007:1* puts "Items count: #{items.length}" +irb(main):008:1* puts "Items: #{items.join(', ')}" +irb(main):009:1* end +=> :summarize +irb(main):010:0> summarize("Apple", "Banana", "Orange") +Items count: 3 +Items: Apple, Banana, Orange +=> nil +irb(main):011:0> summarize("Book") +Items count: 1 +Items: Book +=> nil +irb(main):012:0> summarize +Items count: 0 +Items: +=> nil +``` + +## 例外処理 + +JavaやPythonの `try-catch-finally` に相当する構文として、Rubyは `begin-rescue-ensure` を提供します。 + +### begin, rescue, ensure + + * `begin`: 例外が発生する可能性のある処理を囲みます。 + * `rescue`: 例外を捕捉(catch)します。捕捉する例外クラスを指定できます。 + * `else`: (Optional) `begin` ブロックで例外が発生しなかった場合に実行されます。 + * `ensure`: (Optional) 例外の有無にかかわらず、最後に必ず実行されます(finally)。 + +```ruby:exception_example.rb +def safe_divide(a, b) + begin + # メインの処理 + result = a / b + rescue ZeroDivisionError => e + # ゼロ除算エラーを捕捉 + puts "Error: ゼロで割ることはできません。" + puts "(#{e.class}: #{e.message})" + result = nil + rescue TypeError => e + # 型エラーを捕捉 + puts "Error: 数値以外が使われました。" + puts "(#{e.class}: #{e.message})" + result = nil + else + # 例外が発生しなかった場合 + puts "計算成功: #{result}" + ensure + # 常に実行 + puts "--- 処理終了 ---" + end + + return result +end + +safe_divide(10, 2) +safe_divide(10, 0) +safe_divide(10, "a") +``` + +```ruby-exec:exception_example.rb +計算成功: 5 +--- 処理終了 --- +Error: ゼロで割ることはできません。 +(ZeroDivisionError: divided by 0) +--- 処理終了 --- +Error: 数値以外が使われました。 +(TypeError: String can't be coerced into Integer) +--- 処理終了 --- +``` + +> **補足:** `def` ... `end` のメソッド定義内では、`begin` と `end` は省略可能です。 + +### raise (例外の発生) + +`raise` を使って、意図的に例外を発生(throw)させることができます。 + +```ruby-repl:7 +irb(main):001:0> def check_age(age) +irb(main):002:1* if age < 0 +irb(main):003:2* # raise "エラーメッセージ" +irb(main):004:2* # raise 例外クラス, "エラーメッセージ" +irb(main):005:2* raise ArgumentError, "年齢は負の値にできません" +irb(main):006:2* end +irb(main):007:1* puts "年齢は #{age} 歳です" +irb(main):008:1* end +=> :check_age +irb(main):009:0> check_age(20) +年齢は 20 歳です +=> nil +irb(main):010:0> check_age(-5) +(irb):5:in `check_age': 年齢は負の値にできません (ArgumentError) + from (irb):10:in `
' + ... +``` + +## この章のまとめ + + * Rubyの制御構造(`if`, `case`)は**式**であり、値を返します。 + * `if !` の代わりに `unless` を、`while !` の代わりに `until` を使うことで、否定条件を読みやすく記述できます。 + * メソッドの戻り値は、`return` を使わずとも**最後に評価された式**が自動的に返されます。 + * メソッドの引数は、**デフォルト引数**、**キーワード引数** (`name:`), **可変長引数** (`*args`) を駆使することで、非常に柔軟に定義できます。 + * 例外処理は `begin`, `rescue` (catch), `ensure` (finally) で行い、`raise` で意図的に例外を発生させます。 + +### 練習問題1: 評価メソッドの作成 + +生徒の点数(0〜100)を受け取り、以下の基準で評価(文字列)を返すメソッド `evaluate_score(score)` を作成してください。 + + * 90点以上: "A" + * 70点〜89点: "B" + * 50点〜69点: "C" + * 50点未満: "D" + * 0未満または100を超える場合: `ArgumentError` を `raise` してください。 + +`case` 文と `raise` を使用して実装してください。 + +```ruby:practice3_1.rb +``` + +```ruby-exec:practice3_1.rb +``` + +### 練習問題2: 柔軟なログ出力メソッド + +ログメッセージ(必須)と、オプションとしてログレベル(キーワード引数 `level:`)およびタグ(可変長引数 `tags`)を受け取るメソッド `logger` を作成してください。 + + * メソッドシグネチャ: `def logger(message, level: "INFO", *tags)` + * 実行例: + * `logger("Server started")` + * 出力: `[INFO] Server started` + * `logger("User login failed", level: "WARN", "security", "auth")` + * 出力: `[WARN] (security, auth) User login failed` + * `logger("DB connection lost", level: "ERROR", "database")` + * 出力: `[ERROR] (database) DB connection lost` + +(ヒント: タグの配列 `tags` が空でないかを確認し、`join` メソッドを使って整形してください。) + +```ruby:practice3_2.rb +``` + +```ruby-exec:practice3_2.rb +``` diff --git a/public/docs/ruby-4.md b/public/docs/ruby-4.md new file mode 100644 index 0000000..be8605f --- /dev/null +++ b/public/docs/ruby-4.md @@ -0,0 +1,181 @@ +# 第4章: すべてがオブジェクト + +Rubyの設計思想における最も重要かつ強力なコンセプトの一つは、「すべてがオブジェクトである」という点です。他の言語、例えばJavaやC++では、数値(int, double)や真偽値(boolean)は「プリミティブ型」として扱われ、オブジェクトとは区別されます。 + +しかしRubyでは、`5` のような数値も、`"hello"` のような文字列も、そして `nil` さえも、すべてがメソッド(振る舞い)を持つオブジェクトです。 + +## 🎯 Rubyの核心: 5.times の衝撃 + +他の言語の経験者がRubyに触れて最初に驚くことの一つが、以下のようなコードが動作することです。 + +```ruby-repl:1 +irb(main):001:0> 5.times do +irb(main):002:1* print "Ruby! " +irb(main):003:1> end +Ruby! Ruby! Ruby! Ruby! Ruby! => 5 +``` + +`5` という数値リテラルが `.times` というメソッドを呼び出しています。これは、`5` が単なる値ではなく、`Integer` クラスのインスタンス(オブジェクト)だからです。 + +同様に、文字列もオブジェクトです。 + +```ruby-repl:2 +irb(main):001:0> "hello, world".upcase +=> "HELLO, WORLD" +irb(main):002:0> "hello, world".length +=> 12 +``` + +`"hello, world"` という `String` オブジェクトが、`upcase` や `length` というメソッド(メッセージ)に応答しています。 + +`.class` メソッドを使うと、そのオブジェクトがどのクラスに属しているかを確認できます。 + +```ruby-repl:3 +irb(main):001:0> 5.class +=> Integer +irb(main):002:0> "hello".class +=> String +irb(main):003:0> 3.14.class +=> Float +``` + +## 👻 nil オブジェクト: 無ですらオブジェクト + +Rubyには「何もない」「無効」な状態を示す `nil` という特別な値があります。これは他の言語における `null` や `None` に相当します。 + +しかし、Rubyの哲学を徹底している点は、この `nil` ですらオブジェクトであるということです。 + +```ruby-repl:4 +irb(main):001:0> nil.class +=> NilClass +``` + +`nil` は `NilClass` という専用クラスの唯一のインスタンスです。オブジェクトであるため、`nil` もメソッドを持ちます。 + +```ruby-repl:5 +irb(main):001:0> nil.nil? +=> true +irb(main):002:0> "hello".nil? +=> false +irb(main):003:0> nil.to_s +=> "" +irb(main):004:0> nil.to_i +=> 0 +``` + +`nil` がメソッドを持つことで、`null` チェックに起因するエラー(例えば `null.someMethod()` のような呼び出しによるエラー)を避けやすくなり、より安全で流暢なコードが書ける場合があります。 + +## 📨 メソッド呼び出しの仕組み: メッセージパッシング + +Rubyのメソッド呼び出し `オブジェクト.メソッド名(引数)` は、厳密には「**メッセージパッシング**」という概念に基づいています。 + +`5.times` というコードは、以下のように解釈されます。 + +1. レシーバ(受信者): `5` という `Integer` オブジェクト +2. メッセージ: `:times` というシンボル(メソッド名) +3. `5` オブジェクトに `:times` というメッセージを送る。 +4. `5` オブジェクト(の所属する `Integer` クラス)は、そのメッセージを解釈し、関連付けられた処理(ブロックを5回実行する)を実行する。 + +この考え方は、オブジェクト指向の「カプセル化(オブジェクトが自身の振る舞いを決定する)」を強力にサポートします。`+` などの演算子でさえ、実際にはメソッド呼び出しのシンタックスシュガー(糖衣構文)です。 + +```ruby-repl:6 +irb(main):001:0> 10 + 3 +=> 13 +irb(main):002:0> 10.+(3) # 内部的にはこれと同じ +=> 13 +``` + +## 🛠️ よく使う組み込みクラスのメソッド + +すべてがオブジェクトであるため、Rubyは基本的なデータ型に対して非常に多くの便利なメソッドを標準で提供しています。 + +### String (文字列) + +`String` クラスには、テキスト操作のための豊富なメソッドが用意されています。 + +```ruby:string_methods.rb +text = " ruby is convenient " + +# 先頭と末尾の空白を除去 +cleaned_text = text.strip +puts "Strip: '#{cleaned_text}'" + +# 先頭の文字を大文字に +puts "Capitalize: #{cleaned_text.capitalize}" + +# "convenient" を "powerful" に置換 +puts "Gsub: #{cleaned_text.gsub("convenient", "powerful")}" + +# "ruby" という文字列で始まっているか? +puts "Start with 'ruby'?: #{cleaned_text.start_with?("ruby")}" + +# 単語に分割 (配列が返る) +words = cleaned_text.split(" ") +p words # p はデバッグ用の表示メソッド +``` + +```ruby-exec:string_methods.rb +Strip: 'ruby is convenient' +Capitalize: Ruby is convenient +Gsub: ruby is powerful +Start with 'ruby'?: true +["ruby", "is", "convenient"] +``` + +### Integer / Float (数値) + +数値クラス (総称して `Numeric`) も便利なメソッドを持っています。 + +```ruby-repl:7 +irb(main):001:0> # Integer +irb(main):002:0> 10.even? +=> true +irb(main):003:0> 10.odd? +=> false +irb(main):004:0> 5.to_s +=> "5" +irb(main):005:0> 5.to_f +=> 5.0 + +irb(main):006:0> # Float +irb(main):007:0> 10.5.round +=> 11 +irb(main):008:0> 10.5.floor # 切り捨て +=> 10 +irb(main):009:0> 10.5.ceil # 切り上げ +=> 11 +irb(main):010:0> (10.5).to_i +=> 10 +``` + +## 📜 この章のまとめ + + * Rubyでは、数値、文字列、`nil` を含むすべてが **オブジェクト** です。 + * すべてのオブジェクトは **クラス** に属しています(例: `5` は `Integer` クラス)。 + * オブジェクトであるため、すべての値は **メソッド** を持つことができます(例: `5.times`, `"hello".upcase`)。 + * メソッド呼び出しは、オブジェクトへの **メッセージパッシング** として理解されます。 + * `nil` も `NilClass` のオブジェクトであり、メソッドを持ちます。 + +### 練習問題1: 文字列の操作 +変数 `sentence = " Welcome to the Ruby World! "` があります。`String` のメソッドを組み合わせて、最終的に `"WELCOME, RUBY"` という文字列をコンソールに出力してください。 + + * ヒント: `strip`, `upcase`, `gsub` (または `sub`), `slice` (またはインデックスアクセス `[]`) などが使えます。 + +```ruby:practice4_1.rb +sentence = " Welcome to the Ruby World! " +``` + +```ruby-exec:practice4_1.rb +``` + + +### 練習問題2: 数値と判定 + +`Float` の値 `123.456` があります。この値を四捨五入して整数(`Integer`)にした後、その整数が偶数(even)か奇数(odd)かを判定して、`"Result is even"` または `"Result is odd"` と出力するコードを書いてください。 + +```ruby:practice4_2.rb +value = 123.456 +``` + +```ruby-exec:practice4_2.rb +``` diff --git a/public/docs/ruby-5.md b/public/docs/ruby-5.md new file mode 100644 index 0000000..0dadc7d --- /dev/null +++ b/public/docs/ruby-5.md @@ -0,0 +1,206 @@ +# 第5章: コレクション (Array, Hash, Range) + +Rubyの強力な機能の一つに、柔軟で直感的なコレクション(データを集めて格納するオブジェクト)があります。他の言語でのListやMap、Dictionaryに相当するものを学びましょう。この章では、`Array`(配列)、`Hash`(ハッシュ)、そして `Range`(範囲)を扱います。 + +## 配列 (Array) + +Rubyの `Array` は、他の言語における動的配列やリストに似ています。順序付けられた要素のコレクションであり、異なるデータ型の要素を混在させることができます。 + +### 生成と操作 + +配列は `[]` (角括弧) を使って生成します。 + +```ruby-repl:1 +irb(main):001:0> numbers = [1, 2, 3, 4, 5] +=> [1, 2, 3, 4, 5] +irb(main):002:0> mixed = [1, "hello", true, 3.14] # 型の混在が可能 +=> [1, "hello", true, 3.14] +irb(main):003:0> empty_array = [] +=> [] +``` + +要素へのアクセスは `[index]` を使います。Rubyのインデックスは0から始まり、**負のインデックス**(末尾からのアクセス)をサポートしているのが特徴です。 + +```ruby-repl:2 +irb(main):004:0> numbers[0] # 最初の要素 +=> 1 +irb(main):005:0> numbers[-1] # 末尾の要素 +=> 5 +irb(main):006:0> numbers[-2] # 末尾から2番目の要素 +=> 4 +``` + +### 要素の追加と削除 + +要素の追加には `<<` (shovel演算子) や `push` メソッドを使います。 `pop` は末尾の要素を削除し、それを返します。 + +```ruby-repl:3 +irb(main):007:0> fruits = ["apple", "banana"] +=> ["apple", "banana"] +irb(main):008:0> fruits << "cherry" # << (shovel) は高速で一般的 +=> ["apple", "banana", "cherry"] +irb(main):009:0> fruits.push("orange") +=> ["apple", "banana", "cherry", "orange"] +irb(main):010:0> last_fruit = fruits.pop +=> "orange" +irb(main):011:0> fruits +=> ["apple", "banana", "cherry"] +``` + +### 便利なメソッド + +`Array` には非常に多くの便利なメソッドが用意されています。 + +```ruby-repl:4 +irb(main):012:0> fruits.length # 要素数 +=> 3 +irb(main):013:0> fruits.include?("banana") # 要素が含まれているか +=> true +irb(main):014:0> fruits.sort # ソートされた新しい配列を返す +=> ["apple", "banana", "cherry"] +irb(main):015:0> fruits.first # 最初の要素 +=> "apple" +irb(main):016:0> fruits.last # 最後の要素 +=> "cherry" +``` + +## ハッシュ (Hash) + +`Hash` は、キーと値のペアを格納するコレクションです。他の言語のMap、Dictionary、連想配列に相当します。 + +### 2種類のシンタックス + +Rubyのハッシュには2つの主要な記法があります。 + +#### 1\. 旧シンタックス (Rocket Syntax) + +`=>`(ハッシュロケット)を使う記法です。キーには**任意のオブジェクト**(文字列、数値、シンボルなど)を使用できます。 + +```ruby-repl:5 +irb(main):001:0> # キーが文字列の場合 +irb(main):002:0> user_profile = { "name" => "Alice", "age" => 30 } +=> {"name"=>"Alice", "age"=>30} +irb(main):003:0> user_profile["name"] +=> "Alice" +``` + +#### 2\. 新シンタックス (JSON-like Syntax) + +Ruby 1.9から導入された、より簡潔な記法です。JavaScriptのオブジェクトリテラルに似ています。 + +> **注意:** この記法を使うと、**キーは自動的にシンボル (Symbol) になります**。 + +```ruby-repl:6 +irb(main):004:0> # 新シンタックス (キーはシンボルになる) +irb(main):005:0> user_profile_new = { name: "Bob", age: 25 } +=> {:name=>"Bob", :age=>25} +irb(main):006:0> # アクセス時もシンボル (:name) を使う +irb(main):007:0> user_profile_new[:name] +=> "Bob" +``` + +現在では、キーが固定されている場合は、シンボルを使った新シンタックスが好まれます。 + +## 範囲 (Range) + +`Range` は、連続する値のシーケンスを表すオブジェクトです。`for` ループや `case` 文での条件分岐によく使われます。 + +範囲の作成には `(start..end)` と `(start...end)` の2つの形式があります。 + +### `..` (終端を含む) + +`..`(ドット2つ)は、終端の値を含む範囲を作成します。 + +```ruby-repl:9 +irb(main):001:0> inclusive_range = (1..10) # 1から10まで (10を含む) +=> 1..10 +irb(main):002:0> inclusive_range.to_a # to_aで配列に変換できる +=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +irb(main):003:0> inclusive_range.include?(10) +=> true +``` + +### `...` (終端を含まない) + +`...`(ドット3つ)は、終端の値を含まない(未満の)範囲を作成します。 + +```ruby-repl:10 +irb(main):004:0> exclusive_range = (1...10) # 1から10まで (10を含まない) +=> 1...10 +irb(main):005:0> exclusive_range.to_a +=> [1, 2, 3, 4, 5, 6, 7, 8, 9] +irb(main):006:0> exclusive_range.include?(10) +=> false +``` + +### 範囲の活用例 + +`Range` は `case` 文と組み合わせると非常に強力です。 + +```ruby:grade_checker.rb +def assign_grade(score) + case score + when (90..100) + "A" + when (80...90) # 80は含むが90は含まない (80-89) + "B" + when (60...80) + "C" + else + "F" + end +end + +puts "Score 95: #{assign_grade(95)}" +puts "Score 90: #{assign_grade(90)}" +puts "Score 89: #{assign_grade(89)}" +puts "Score 60: #{assign_grade(60)}" +puts "Score 59: #{assign_grade(59)}" +``` + +```ruby-exec:grade_checker.rb +Score 95: A +Score 90: A +Score 89: B +Score 60: C +Score 59: F +``` + +## この章のまとめ + + * **Array**: `[]` で作成する順序付きリスト。`<<` で追加、`pop` で取り出し、`[-1]` で末尾にアクセスできます。 + * **Hash**: `{}` で作成するキー/バリューペア。 + * **Symbol**: `:name` のようにコロンで始まる識別子。イミュータブルで高速なため、ハッシュのキーに最適です。 + * **Hashのシンタックス**: キーがシンボルの場合、`{ key: "value" }` というモダンな記法が使えます。 + * **Range**: `(1..10)`(含む)と `(1...10)`(含まない)があり、連続したシーケンスを表現します。 + +### 練習問題1: ショッピングカートの管理 + +あなたのショッピングカートを表現する配列 `cart` があります。 +`cart` は、商品情報を表すハッシュの配列です。 +以下の操作を行ってください。 + +1. `cart` に `{ name: "Orange", price: 120 }` を追加する。 +2. `cart` の最初の商品の名前 (`"Apple"`) を表示する。 + +```ruby:practice5_1.rb +cart = [{ name: "Apple", price: 100 }, { name: "Banana", price: 80 }] +``` + +```ruby-exec:practice5_1.rb +``` + +### 練習問題2: ハッシュの操作 + +ユーザーの設定を保存するハッシュ `settings` を作成してください。 + +* キーにはシンボルを使用します (`:theme`, `:notifications`)。 +* `:theme` の初期値は `:light`、`:notifications` の初期値は `true` とします。 +* `settings` を作成した後、`:theme` の値を `:dark` に更新してください。 + +```ruby:practice5_2.rb +settings = +``` + +```ruby-exec:practice5_2.rb +``` diff --git a/public/docs/ruby-6.md b/public/docs/ruby-6.md new file mode 100644 index 0000000..04f17e2 --- /dev/null +++ b/public/docs/ruby-6.md @@ -0,0 +1,307 @@ +# 第6章: ブロックとイテレータ - Rubyの最重要機能 + +Rubyの学習において、**ブロック (Block)** は最も重要で強力な機能の一つです。他言語の経験者にとって、これはラムダ式や無名関数、クロージャに似た概念ですが、Rubyではこれが言語構文の核に深く組み込まれています。 + +この章では、ブロックの使い方と、ブロックを活用する「イテレータ」と呼ばれるメソッドを学びます。 + +## ブロック構文: do...end と {} + +ブロックとは、メソッド呼び出しに渡すことができる**コードの塊**です。メソッド側は、受け取ったそのコードの塊を好きなタイミングで実行できます。 + +ブロックには2種類の書き方があります。 + +1. **`{ ... }` (波括弧)**: 通常、1行で完結する場合に使われます。 +2. **`do ... end`**: 複数行にわたる処理を書く場合に使われます。 + +どちらも機能的にはほぼ同じです。最も簡単な例は、指定した回数だけブロックを実行する `times` メソッドです。 + +```ruby-repl:1 +irb(main):001:0> 3.times { puts "Hello!" } +Hello! +Hello! +Hello! +=> 3 + +irb(main):002:0> 3.times do +irb(main):003:1* puts "Ruby is fun!" +irb(main):004:1> end +Ruby is fun! +Ruby is fun! +Ruby is fun! +=> 3 +``` + +`3.times` というメソッド呼び出しの後ろに `{ ... }` や `do ... end` で囲まれたコードブロックを渡しています。`times` メソッドは、そのブロックを3回実行します。 + +## 代表的なイテレータ + +Rubyでは、コレクション(配列やハッシュなど)の各要素に対して処理を行うメソッドを**イテレータ (Iterator)** と呼びます。イテレータは通常、ブロックを受け取って動作します。 + +代表的なイテレータを見ていきましょう。 + +### each + +`each` は、コレクションの各要素を順番に取り出してブロックを実行します。他言語の `foreach` ループに最も近いものです。 + +`|n|` の部分は**ブロック引数**と呼ばれ、イテレータが取り出した要素(この場合は配列の各要素)を受け取ります。 + +```ruby-repl:2 +irb(main):001:0> numbers = [1, 2, 3] +=> [1, 2, 3] + +irb(main):002:0> numbers.each do |n| +irb(main):003:1* puts "Current number is #{n}" +irb(main):004:1> end +Current number is 1 +Current number is 2 +Current number is 3 +=> [1, 2, 3] +``` + +> **Note:** `each` メソッドの戻り値は、元の配列 (`[1, 2, 3]`) 自身です。`each` はあくまで「繰り返すこと」が目的であり、ブロックの実行結果は利用しません。 + +### map (collect) + +`map` は、各要素に対してブロックを実行し、その**ブロックの戻り値**を集めた**新しい配列**を返します。 + +```ruby-repl:3 +irb(main):005:0> numbers = [1, 2, 3] +=> [1, 2, 3] + +irb(main):006:0> doubled = numbers.map { |n| n * 2 } +=> [2, 4, 6] + +irb(main):007:0> puts doubled.inspect +[2, 4, 6] +=> nil + +irb(main):008:0> puts numbers.inspect # 元の配列は変更されない +[1, 2, 3] +=> nil +``` + +`map` は、元の配列を変換した新しい配列が欲しい場合に非常に便利です。 + +### select (filter) + +`select` は、各要素に対してブロックを実行し、ブロックの戻り値が**真 (true)** になった要素だけを集めた**新しい配列**を返します。 + +```ruby-repl:4 +irb(main):009:0> numbers = [1, 2, 3, 4, 5, 6] +=> [1, 2, 3, 4, 5, 6] + +irb(main):010:0> evens = numbers.select { |n| n.even? } # n.even? は n % 2 == 0 と同じ +=> [2, 4, 6] +``` + +### find (detect) + +`find` は、ブロックの戻り値が**真 (true)** になった**最初の要素**を返します。見つからなければ `nil` を返します。 + +```ruby-repl:5 +irb(main):011:0> numbers = [1, 2, 3, 4, 5, 6] +=> [1, 2, 3, 4, 5, 6] + +irb(main):012:0> first_even = numbers.find { |n| n.even? } +=> 2 + +irb(main):013:0> over_10 = numbers.find { |n| n > 10 } +=> nil +``` + +## Enumerableモジュール:イテレーションの力 + +`each`, `map`, `select`, `find` といった便利なメソッドは、実は `Enumerable`(エニューメラブル)という**モジュール**によって提供されています。 + +`Enumerable` はRubyの「Mix-in(ミックスイン)」機能の代表例です。これは、クラスに「混ぜ込む」ことで、そのクラスのインスタンスに特定の機能(メソッド群)を追加する仕組みです。 + +`Enumerable` をMix-inするクラス(例えば `Array` や `Hash`, `Range`)が満たすべき契約はただ一つ、**`each` メソッドを実装すること**です。 + +`each` メソッドさえ定義されていれば、`Enumerable` モジュールは `each` を使って `map`, `select`, `find`, `sort`, `count` など、数十もの便利なイテレーションメソッドを自動的に提供してくれます。 + +例えば、`Array` クラスは `each` を持っています。 + +```ruby-repl:6 +irb(main):014:0> numbers = [1, 2, 3] +=> [1, 2, 3] +# numbers (Array) は each を持っているので... +irb(main):015:0> numbers.map { |n| n * 2 } # map が使える +=> [2, 4, 6] +irb(main):016:0> numbers.select { |n| n.odd? } # select が使える +=> [1, 3] +``` + +これは、自分で新しいコレクションクラスを作った場合でも同様です。(`include` については後の「モジュールとMix-in」の章で詳しく学びます) + +```ruby:my_collection.rb +# Enumerableモジュールを include する +class MyCollection + include Enumerable # これがMix-in + + def initialize(items) + @items = items + end + + # Enumerable のために each メソッドを定義する + def each + @items.each do |item| + yield(item) # ブロックに要素を渡す + end + end +end + +collection = MyCollection.new([10, 20, 30]) + +# each を定義しただけで、map が使える! +doubled = collection.map { |x| x * 2 } +puts "Map result: #{doubled.inspect}" + +# select も使える! +selected = collection.select { |x| x > 15 } +puts "Select result: #{selected.inspect}" +``` + +```ruby-exec:my_collection.rb +Map result: [20, 40, 60] +Select result: [20, 30] +``` + +このように、Rubyのイテレータの強力さは `Enumerable` モジュールによって支えられています。Rubyでは、**「`each` メソッドを持つものは、すべて `Enumerable` である(あるいはそう振る舞える)」**という考え方が非常に重要です。 + +## for ループとの比較 + +他言語経験者の方は、`for` ループを使いたくなるかもしれません。 + +```c +// C や Java の for ループ +for (int i = 0; i < 3; i++) { + printf("Hello\n"); +} +``` + +Rubyにも `for` 構文は存在します。 + +```ruby-repl:7 +irb(main):014:0> numbers = [1, 2, 3] +=> [1, 2, 3] + +irb(main):015:0> for num in numbers +irb(main):016:1* puts num +irb(main):017:1> end +1 +2 +3 +=> [1, 2, 3] +``` + +しかし、Rubyの世界では `for` ループは**ほとんど使われません**。なぜなら、`for` は内部的に `each` メソッドを呼び出しているに過ぎないからです。 + +Rubyプログラマは、`for` よりも `each` などのイテレータをブロックと共に使うことを圧倒的に好みます。イテレータの方が、何をしているか(単なる繰り返し、変換、選択など)がメソッド名 (`each`, `map`, `select`) から明確であり、コードが読みやすくなるためです。 + +## ブロック引数とブロックの戻り値 + +すでに出てきたように、ブロックは `| ... |` を使って引数を受け取ることができます。 + +```ruby-repl:8 +irb(main):018:0> ["Alice", "Bob"].each do |name| +irb(main):019:1* puts "Hello, #{name}!" +irb(main):020:1> end +Hello, Alice! +Hello, Bob! +=> ["Alice", "Bob"] +``` + +また、ブロックも(Rubyのすべての式と同様に)戻り値を持ちます。ブロックの戻り値とは、**ブロック内で最後に評価された式の値**です。 + + * `each` はブロックの戻り値を**無視**します。 + * `map` はブロックの戻り値を**集めて新しい配列**にします。 + * `select` はブロックの戻り値が**真か偽か**を判定に使います。 + +```ruby-repl:9 +irb(main):021:0> result = [1, 2].map do |n| +irb(main):022:1* m = n * 10 # mは 10, 20 +irb(main):023:1* m + 5 # ブロックの戻り値 (15, 25) +irb(main):024:1> end +=> [15, 25] +``` + +## yield:ブロックを受け取るメソッド + +では、どうすればブロックを受け取るメソッドを自分で作れるのでしょうか? +それには `yield` というキーワードを使います。 + +メソッド内で `yield` が呼び出されると、そのメソッドに渡されたブロックが実行されます。 + +```ruby:yield_basic.rb +def simple_call + puts "メソッド開始" + yield # ここでブロックが実行される + puts "メソッド終了" +end + +simple_call do + puts "ブロック内の処理です" +end +``` + +```ruby-exec:yield_basic.rb +メソッド開始 +ブロック内の処理です +メソッド終了 +``` + +`yield` はブロックに引数を渡すこともできます。 + +```ruby:yield_with_args.rb +def call_with_name(name) + puts "メソッド開始" + yield(name) # ブロックに "Alice" を渡す + yield("Bob") # ブロックに "Bob" を渡す + puts "メソッド終了" +end + +call_with_name("Alice") do |n| + puts "ブロックが #{n} を受け取りました" +end +``` + +```ruby-exec:yield_with_args.rb +メソッド開始 +ブロックが Alice を受け取りました +ブロックが Bob を受け取りました +メソッド終了 +``` + +`each` や `map` のようなイテレータは、内部でこの `yield` を使って、コレクションの各要素をブロックに渡しながら実行しているのです。 + +## この章のまとめ + + * **ブロック**は、メソッドに渡せるコードの塊で、`{}`(1行)または `do...end`(複数行)で記述します。 + * **イテレータ**は、ブロックを受け取り、要素の繰り返し処理を行うメソッドです(`each`, `map`, `select` など)。 + * **Enumerableモジュール**は、 `each` を実装するクラスに `map` や `select` などの強力なイテレーション機能を提供します。 + * Rubyでは `for` ループよりもイテレータが好まれます。 + * ブロックは `|arg|` で引数を受け取ることができ、ブロックの最後の式の値が戻り値となります。 + * 自作メソッド内で `yield` を使うと、渡されたブロックを実行できます。 + +### 練習問題1 + +数値の配列 `[1, 2, 3, 4, 5]` があります。`map` イテレータとブロックを使って、各要素を文字列に変換し(例: `1` → `"1"`)、 `"1"`, `"2"`, `"3"`, `"4"`, `"5"` という文字列の配列を作成してください。 + +```ruby:practice6_1.rb +array = [1, 2, 3, 4, 5] + +``` + +```ruby-exec:practice6_1.rb +``` + +### 練習問題2 +文字列の配列 `["apple", "banana", "cherry", "date"]` があります。`select` イテレータとブロックを使って、文字数が5文字以上の果物だけを抽出した新しい配列(`["apple", "banana", "cherry"]`)を作成してください。 + +```ruby:practice6_2.rb +array = ["apple", "banana", "cherry", "date"] + +``` + +```ruby-exec:practice6_2.rb +``` diff --git a/public/docs/ruby-7.md b/public/docs/ruby-7.md new file mode 100644 index 0000000..934ce0d --- /dev/null +++ b/public/docs/ruby-7.md @@ -0,0 +1,381 @@ +# 第7章: クラスとオブジェクト(基本) + +Rubyは純粋なオブジェクト指向言語であり、第4章「すべてがオブジェクト」で学んだように、数値や文字列さえもオブジェクトです。この章では、それらのオブジェクトの「設計図」である**クラス**を定義する方法について学びます。 + +他のオブジェクト指向言語(Java, Python, C\#など)の経験があれば、概念は馴染み深いはずです。Ruby特有の構文(`@`や`attr_*`など)に注目してください。 + +## 💴 クラス定義: class, initialize + +Rubyでは、`class`キーワードを使ってクラスを定義します。クラス名は慣習として**大文字**で始めます(例: `MyClass`)。 + +`new`メソッドが呼ばれたときに実行される特別なメソッドが `initialize` です。これは他の言語における**コンストラクタ**に相当し、インスタンスの初期化処理を行います。 + +```ruby:user.rb +# クラス名はアッパーキャメルケース(PascalCase)で記述します +class User + # newが呼ばれた際に自動的に実行される初期化メソッド + def initialize(name, age) + # インスタンス変数は @ で始める + @name = name + @age = age + puts "Userオブジェクトが作成されました!" + end +end + +# クラスからインスタンスを生成 +# User.new は initialize メソッドを呼び出す +user1 = User.new("Alice", 30) +user2 = User.new("Bob", 25) + +p user1 +p user2 +``` + +```ruby-exec:user.rb +Userオブジェクトが作成されました! +Userオブジェクトが作成されました! + + +``` + +## 🏃‍♂️ インスタンス変数 (@var) とインスタンスメソッド + +### インスタンス変数 + +`@`で始まる変数(例: `@name`)は**インスタンス変数**です。 + + * そのクラスのインスタンス(オブジェクト)ごとに個別に保持されます。 + * `initialize`や他のインスタンスメソッド内で定義・参照されます。 + * **デフォルトで外部から直接アクセスすることはできません(カプセル化)**。 + +### インスタンスメソッド + +`def`で定義されたメソッド(`initialize`を除く)が**インスタンスメソッド**です。これらはインスタンスの「振る舞い」を定義し、そのインスタンスのインスタンス変数(`@var`)にアクセスできます。 + +```ruby:user_greet.rb +class User + def initialize(name, age) + @name = name + @age = age + end + + # インスタンスメソッドの定義 + def greet + # メソッド内からインスタンス変数(@name, @age)を参照 + puts "こんにちは、#{@name}さん (#{@age}歳) です。" + end +end + +user1 = User.new("Alice", 30) + +# インスタンスメソッドの呼び出し +user1.greet +``` + +```ruby-exec:user_greet.rb +こんにちは、Aliceさん (30歳) です。 +``` + +## 🔐 アクセサ: attr\_reader, attr\_writer, attr\_accessor + +前述の通り、`@name`のようなインスタンス変数は外部から直接参照・変更できません。 + +```ruby:access_error.rb +class User + def initialize(name) + @name = name + end +end + +user = User.new("Alice") +p user.name #=> NoMethodError +user.name = "Bob" #=> NoMethodError +``` + +```ruby-exec:access_error.rb +NoMethodError (undefined method `name'...) +``` + + +外部からアクセスさせるためには、**アクセサメソッド**(ゲッターとセッター)を定義する必要があります。 + +### 手動での定義 + +JavaやC\#のように、ゲッターとセッターを明示的に書くこともできます。 + +```ruby:manual_accessor.rb +class Product + def initialize(name) + @name = name + end + + # ゲッター (値の読み取り) + def name + @name + end + + # セッター (値の書き込み) + # メソッド名が = で終わるのが特徴 + def name=(new_name) + @name = new_name + end +end + +item = Product.new("Laptop") +puts item.name # ゲッター(item.name)の呼び出し +item.name = "Desktop" # セッター(item.name=)の呼び出し +puts item.name +``` + +```ruby-exec:manual_accessor.rb +Laptop +Desktop +``` + +### `attr_*` による自動定義 + +Rubyでは、上記のような定型的なアクセサメソッドを自動で定義するための便利な「マクロ」が用意されています。これらはクラス定義のトップレベルで使います。 + + * `attr_reader :var` : ゲッター(読み取り専用)を定義します。 + * `attr_writer :var` : セッター(書き込み専用)を定義します。 + * `attr_accessor :var` : ゲッターとセッターの両方を定義します。 + +引数にはインスタンス変数名の`@`を除いた**シンボル**(`:`から始まる名前)を渡します。 + +```ruby:auto_accessor.rb +class Product + # @name のゲッターとセッターを自動定義 + attr_accessor :name + # @price のゲッターのみを自動定義 (読み取り専用) + attr_reader :price + # @stock のセッターのみを自動定義 (書き込み専用) + attr_writer :stock + + def initialize(name, price, stock) + @name = name + @price = price + @stock = stock + end + + def summary + # ゲッターは self.price とも書けるが、 + # クラス内部では @price と直接アクセスするのが一般的 + "商品: #{@name}, 価格: #{@price}円" + end +end + +item = Product.new("Mouse", 3000, 50) + +# attr_accessor +puts item.name # ゲッター +item.name = "Keyboard" # セッター +puts item.name + +# attr_reader +puts item.price # ゲッター +# item.price = 3500 # => NoMethodError (undefined method `price=') + +# attr_writer +# puts item.stock # => NoMethodError (undefined method `stock') +item.stock = 100 # セッター + +puts item.summary +``` + +```ruby-exec:auto_accessor.rb +Mouse +Keyboard +3000 +商品: Keyboard, 価格: 3000円 +``` + +## 🏢 クラス変数 (@@var) とクラスメソッド (self.method\_name) + +### クラス変数 (@@var) + +`@@`で始まる変数(例: `@@count`)は**クラス変数**です。 + + * インスタンスごとではなく、**クラス全体で共有**されます。 + * そのクラスのすべてのインスタンスから参照・変更が可能です。 + * (注意)継承した場合、子クラスとも共有されるため、意図しない動作の原因になることもあり、使用には注意が必要です。 + +### クラスメソッド (self.method\_name) + +インスタンスではなく、**クラス自体から呼び出すメソッド**です。`def self.メソッド名` のように `self.` をつけて定義します。 + + * `User.new` の `new` も、実はクラスメソッドの一種です。 + * インスタンス変数 (`@var`) にはアクセスできません(インスタンスが存在しないため)。 + * クラス変数 (`@@var`) にはアクセスできます。 + * ファクトリメソッド(特定のパターンのインスタンスを生成するメソッド)や、クラス全体に関わる操作(例: 総数のカウント)によく使われます。 + +```ruby:counter.rb +class Counter + # クラス変数(クラス全体で共有) + @@total_count = 0 + + attr_reader :id + + def initialize(id) + @id = id + # インスタンスが作られるたびにクラス変数を増やす + @@total_count += 1 + end + + # クラスメソッド (self. をつける) + # クラス変数を返す + def self.total_count + @@total_count + end + + # インスタンスメソッド + def report_total + # インスタンスメソッドからもクラス変数を参照できる + "私のIDは #{@id} です。総数は #{@@total_count} です。" + end +end + +# クラスメソッドの呼び出し +puts "初期カウント: #{Counter.total_count}" #=> 0 + +c1 = Counter.new(1) +c2 = Counter.new(2) + +# クラスメソッドの呼び出し +puts "最終カウント: #{Counter.total_count}" #=> 2 + +# インスタンスメソッドの呼び出し +puts c1.report_total #=> 私のIDは 1 です。総数は 2 です。 +puts c2.report_total #=> 私のIDは 2 です。総数は 2 です。 + +# c1.total_count #=> NoMethodError (インスタンスからは呼べない) +``` + +```ruby-exec:counter.rb +初期カウント: 0 +最終カウント: 2 +私のIDは 1 です。総数は 2 です。 +私のIDは 2 です。総数は 2 です。 +``` + +## 👪 継承 (\<) と super + +Rubyは**単一継承**をサポートしています。`<` 記号を使って親クラス(スーパークラス)を指定します。 + +子クラス(サブクラス)は、親クラスのメソッドや変数を引き継ぎます。 + +### `super` + +子クラスで親クラスと同じ名前のメソッドを定義(**オーバーライド**)した際、`super`キーワードを使うと、**親クラスの同名メソッドを呼び出す**ことができます。 + +これは特に `initialize` メソッドで、親クラスの初期化処理を呼び出すために必須となります。 + +```ruby:vehicle.rb +# 親クラス (スーパークラス) +class Vehicle + attr_reader :name + + def initialize(name) + @name = name + puts "Vehicleを初期化中: #{@name}" + end + + def move + puts "#{@name} は移動します。" + end +end + +# 子クラス (サブクラス) +# Vehicle クラスを継承 +class Car < Vehicle + def initialize(name, color) + # super で親クラスの initialize を呼び出す + # (name を渡す) + super(name) + @color = color + puts "Carを初期化中: 色は#{@color}" + end + + # move メソッドをオーバーライド (上書き) + def move + # super で親クラスの move メソッドを呼び出す + super + # Car 固有の処理を追加 + puts "車輪が回転します。" + end +end + +my_car = Car.new("MyCar", "Red") +puts "---" +my_car.move +``` + +```ruby-exec:vehicle.rb +Vehicleを初期化中: MyCar +Carを初期化中: 色はRed +--- +MyCar は移動します。 +車輪が回転します。 +``` + +`super` は引数を省略すると、現在のメソッドが受け取った引数をそのまま親メソッドに渡します。`super()` のように `()` をつけると、引数なしで親メソッドを呼び出します。 + +## 📝 この章のまとめ + + * クラスは `class` キーワードで定義し、インスタンスは `.new` で生成します。 + * `initialize` はインスタンス生成時に呼ばれるコンストラクタです。 + * インスタンス変数は `@` で始まり、インスタンスごとに独立し、デフォルトでプライベートです。 + * `attr_reader`, `attr_writer`, `attr_accessor` は、インスタンス変数へのアクセサ(ゲッター/セッター)を自動定義するマクロです。 + * クラス変数は `@@` で始まり、クラスと全インスタンスで共有されます。 + * クラスメソッドは `def self.メソッド名` で定義し、クラス自体から呼び出します。 + * 継承は `<` で行い、`super` で親クラスの同名メソッドを呼び出します。 + +### 練習問題1: `Book` クラスの作成 + +以下の仕様を持つ `Book` クラスを作成してください。 + +1. `initialize` で `title`(タイトル)と `author`(著者)を受け取る。 +2. `title` と `author` は、インスタンス変数(`@title`, `@author`)に格納する。 +3. `title` と `author` は、どちらも外部から読み取り可能(書き換えは不可)にする。 +4. `info` というインスタンスメソッドを持ち、`"タイトル: [title], 著者: [author]"` という形式の文字列を返す。 + +```ruby:practice7_1.rb +# ここにBookクラスの定義を書いてください + + +book = Book.new("Ruby入門", "Sato") +puts book.info +puts book.title +# book.title = "改訂版" #=> エラー (NoMethodError) になるはず +``` + +```ruby-exec:practice7_1.rb +(実行結果例) +タイトル: Ruby入門, 著者: Sato +Ruby入門 +``` + + +### 練習問題2: 継承を使った `EBook` クラスの作成 + +問題1で作成した `Book` クラスを継承して、以下の仕様を持つ `EBook`(電子書籍)クラスを作成してください。 + +1. `initialize` で `title`, `author`, `file_size`(ファイルサイズ, 例: "10MB")を受け取る。 +2. `title` と `author` の初期化は、`Book` クラスの `initialize` を利用する (`super` を使う)。 +3. `file_size` は外部から読み取り可能にする。 +4. `info` メソッドをオーバーライドし、`"タイトル: [title], 著者: [author] (ファイルサイズ: [file_size])"` という形式の文字列を返す。 + * ヒント: 親クラスの `info` メソッドの結果を `super` で利用すると効率的です。 + +```ruby:practice7_2.rb +require './practice7_1.rb' # 7_1のコードを実行してBookの定義を読み込みます + +# ここにEBookクラスの定義を書いてください + +ebook = EBook.new("実践Ruby", "Tanaka", "2.5MB") +puts ebook.info +puts ebook.title +``` + +```ruby-exec:practice7_2.rb +タイトル: 実践Ruby, 著者: Tanaka (ファイルサイズ: 2.5MB) +実践Ruby +``` diff --git a/public/docs/ruby-8.md b/public/docs/ruby-8.md new file mode 100644 index 0000000..89c2ae3 --- /dev/null +++ b/public/docs/ruby-8.md @@ -0,0 +1,306 @@ +# 第8章: モジュールとミックスイン(オブジェクト指向の拡張) + +Rubyのオブジェクト指向において、クラスの継承は「is-a」(〜である)関係を表現するのに適しています。しかし、「has-a」(〜を持つ)や「can-do」(〜ができる)といった**振る舞い(ビヘイビア)**を複数の異なるクラス間で共有したい場合があります。 + +他の言語では「インターフェース」や「トレイト」で解決するこの問題を、Rubyは**モジュール (Module)** と **ミックスイン (Mix-in)** という強力な仕組みで解決します。 + +## モジュール (module) の2つの役割 + +`module` キーワードで定義されるモジュールには、大きく分けて2つの主要な役割があります。 + +1. **名前空間 (Namespace):** + 関連するクラス、メソッド、定数を一つのグループにまとめ、名前の衝突(コンフリクト)を防ぎます。 +2. **ミックスイン (Mix-in):** + メソッドの集まりを定義し、それをクラスに `include` することで、インスタンスメソッドとして機能を追加します。これはRubyの「多重継承」の代替手段です。 + +## 名前空間としてのモジュール + +プログラムが大規模になると、異なる目的で同じ名前のクラス(例: `Database::User` と `WebApp::User`)を使いたくなることがあります。モジュールは、これらを区別するための「仕切り」として機能します。 + +名前空間内の要素には、`::` (スコープ解決演算子) を使ってアクセスします。 + +```ruby:module_example.rb +module AppUtilities + VERSION = "1.0.0" + + class Logger + def log(msg) + puts "[App log] #{msg}" + end + end + + # モジュールメソッド (self. をつける) + def self.default_message + "Hello from Utility" + end +end + +# 定数へのアクセス +puts AppUtilities::VERSION + +# モジュールメソッドの呼び出し +puts AppUtilities.default_message + +# モジュール内のクラスのインスタンス化 +logger = AppUtilities::Logger.new +logger.log("Initialized.") +``` + +```ruby-exec:module_example.rb +1.0.0 +Hello from Utility +[App log] Initialized. +``` + +## ミックスインとしてのモジュール (include) + +モジュールの最も強力な機能がミックスインです。これにより、クラスは継承ツリーとは無関係に、モジュールの振る舞い(インスタンスメソッド)を取り込むことができます。 + +`include` を使うと、モジュールはクラスの継承チェーン(祖先チェーン)に挿入されます。具体的には、`include` したクラスのスーパークラスの「直前」に挿入されます。 + +```ruby:mix_in_example.rb +# 「飛ぶ」能力を提供するモジュール +module Flyable + def fly + puts "I'm flying! My speed is #{fly_speed}." + end + + # このモジュールは、include したクラスが + # `fly_speed` メソッドを実装していることを期待している +end + +# 「泳ぐ」能力を提供するモジュール +module Swimmable + def swim + puts "I'm swimming!" + end +end + +class Bird + # fly_speed を実装 + def fly_speed + "10km/h" + end +end + +class Duck < Bird + include Flyable # 飛べる + include Swimmable # 泳げる +end + +class Penguin < Bird + include Swimmable # 泳げる (飛べない) +end + +class Airplane + include Flyable # 飛べる (生物ではない) + + def fly_speed + "800km/h" + end +end + +puts "--- Duck ---" +duck = Duck.new +duck.fly +duck.swim + +puts "--- Penguin ---" +penguin = Penguin.new +# penguin.fly #=> NoMethodError +penguin.swim + +puts "--- Airplane ---" +airplane = Airplane.new +airplane.fly +# airplane.swim #=> NoMethodError +``` + +```ruby-exec:mix_in_example.rb +--- Duck --- +I'm flying! My speed is 10km/h. +I'm swimming! +--- Penguin --- +I'm swimming! +--- Airplane --- +I'm flying! My speed is 800km/h. +``` + +`Duck` と `Airplane` は全く異なるクラス(`Bird` のサブクラスと、`Object` のサブクラス)ですが、`Flyable` モジュールを `include` することで `fly` メソッドを共有できています。 + +## include vs extend + +`include` と `extend` は、モジュールのメソッドをどこに追加するかが異なります。 + + * `include`: モジュールのメソッドを、クラスの**インスタンスメソッド**として追加します。 + * `extend`: モジュールのメソッドを、クラスの**クラスメソッド**(特異メソッド)として追加します。 + +```ruby:extend_example.rb +module HelperMethods + def info + "This is a helper method." + end +end + +# --- include の場合 --- +class IncludedClass + include HelperMethods +end + +obj = IncludedClass.new +obj.info # インスタンスメソッドになる +# IncludedClass.info #=> NoMethodError + +# --- extend の場合 --- +class ExtendedClass + extend HelperMethods +end + +ExtendedClass.info # クラスメソッドになる +obj2 = ExtendedClass.new +# obj2.info #=> NoMethodError +``` + +```ruby-exec:extend_example.rb +"This is a helper method." +"This is a helper method." +``` + +## アクセスコントロール (public, private, protected) + +Rubyのアクセスコントロールは、他の言語と少し異なる振る舞い、特に `private` の動作に特徴があります。 + + * `public` (デフォルト) + + * どこからでも呼び出せます。レシーバ(`object.`)を省略しても、明示しても構いません。 + + * `private` + + * **レシーバを明示して呼び出すことができません**。 + * `self.` を付けずに、クラス内部(またはサブクラス)からのみ呼び出せます。 + * 主にクラス内部の詳細を隠蔽(カプセル化)するために使われます。 + + * `protected` + + * `private` と似ていますが、**同じクラス(またはサブクラス)の他のインスタンスをレシーバとして呼び出すことができます**。 + * オブジェクト同士を比較するメソッドなどで使われます。 + +```ruby:access_control_demo.rb +class Wallet + attr_reader :id + + def initialize(id, amount) + @id = id + @balance = amount # private なインスタンス変数 + end + + # public メソッド (外部インターフェース) + def transfer(other_wallet, amount) + if withdraw(amount) + other_wallet.deposit(amount) + puts "Transferred #{amount} from #{self.id} to #{other_wallet.id}" + else + puts "Transfer failed: Insufficient funds in #{self.id}" + end + end + + # protected メソッド (インスタンス間での連携) + protected + + def deposit(amount) + @balance += amount + end + + # private メソッド (内部処理) + private + + def withdraw(amount) + if @balance >= amount + @balance -= amount + true + else + false + end + end +end + +w1 = Wallet.new("Wallet-A", 100) +w2 = Wallet.new("Wallet-B", 50) + +# public メソッドはどこからでも呼べる +w1.transfer(w2, 70) + +puts "w1 ID: #{w1.id}" +# puts "w1 Balance: #{w1.balance}" #=> NoMethodError (attr_reader がないため) + +# private / protected メソッドは外部から直接呼べない +# w1.deposit(100) #=> NoMethodError: protected method `deposit' called... +# w1.withdraw(10) #=> NoMethodError: private method `withdraw' called... +``` + +```ruby-exec:access_control_demo.rb +Transferred 70 from Wallet-A to Wallet-B +w1 ID: Wallet-A +``` + +この例では、`transfer` (public) が内部で `withdraw` (private) を呼び出し、引数で受け取った `other_wallet` の `deposit` (protected) を呼び出しています。`deposit` は `protected` なので、`other_wallet.` というレシーバを明示しても `Wallet` クラス内からは呼び出せます。 + +## この章のまとめ + + * **モジュール**は `module` キーワードで定義され、**名前空間**と**ミックスイン**の2つの役割を持ちます。 + * 名前空間としては、`::` を使って定数やクラスをグループ化し、名前の衝突を防ぎます。 + * ミックスインとしては、`include` することでモジュールのメソッドを**インスタンスメソッド**としてクラスに追加できます。これは多重継承の代わりとなる強力な機能です。 + * `extend` は、モジュールのメソッドを**クラスメソッド**として追加します。 + * `public`, `private`, `protected` でメソッドの可視性を制御します。 + * Rubyの `private` は「レシーバを指定して呼び出せない」というユニークな制約を持ちます。 + +### 練習問題1: カウンター機能のミックスイン + +`Enumerable` モジュール(第6章で少し触れました)のように、`include` したクラスに便利な機能を追加するモジュールを作成します。 + +1. `Counter` というモジュールを定義してください。 +2. `Counter` モジュールは `count_items(item_to_find)` というメソッドを持つものとします。 +3. このメソッドは、`include` したクラスが `items` という名前の配列(`Array`)を返すインスタンスメソッドを持っていることを前提とします。 +4. `count_items` は、その `items` 配列内に `item_to_find` がいくつ含まれているかを返します。 +5. `ShoppingCart` クラスと `WordList` クラスを作成し、両方で `items` メソッドを実装し、`Counter` モジュールを `include` して `count_items` が動作することを確認してください。 + +```ruby:practice8_1.rb +module Counter + +end + +class ShoppingCart + +end + +class WordList + +end + + + +``` + +```ruby-exec:practice8_1.rb +``` + +### 練習問題2: protected を使った比較 + +`protected` のユースケースを理解するための問題です。 + +1. `Score` クラスを作成します。`initialize` で `@value` (得点)をインスタンス変数として保持します。 +2. `higher_than?(other_score)` という `public` なインスタンスメソッドを定義してください。これは、`other_score` (`Score` の別のインスタンス)より自分の `@value` が高ければ `true` を返します。 +3. `higher_than?` メソッドの実装のために、`value` という `protected` メソッドを作成し、`@value` を返すようにしてください。 +4. `higher_than?` の内部では、`self.value > other_score.value` のように `protected` メソッドを呼び出してください。 +5. 2つの `Score` インスタンスを作成し、`higher_than?` が正しく動作することを確認してください。また、`protected` メソッドである `value` をインスタンスの外部から直接呼び出そうとするとエラーになることも示してください。 + +```ruby:practice8_2.rb +class Score + +end + + +``` + +```ruby-exec:practice8_2.rb +``` diff --git a/public/docs/ruby-9.md b/public/docs/ruby-9.md new file mode 100644 index 0000000..9f87b2f --- /dev/null +++ b/public/docs/ruby-9.md @@ -0,0 +1,311 @@ +# 第9章: Proc, Lambda, クロージャ + +これまでの章で、Rubyの強力な機能である「ブロック」を `each` や `map` などのメソッドと共に使ってきました。しかし、ブロックは常にメソッド呼び出しに付随する形でしか使えませんでした。 + +この章では、そのブロックを「オブジェクト」として扱い、変数に代入したり、メソッドの引数として自由に受け渡したりする方法を学びます。これにより、Rubyの表現力はさらに向上します。 + +## ブロックをオブジェクトとして扱う: Proc クラス + +ブロックは、それ自体ではオブジェクトではありません。しかし、Rubyにはブロックをオブジェクト化するための `Proc` クラスが用意されています。 + +`Proc.new` にブロックを渡すことで、`Proc` オブジェクトを作成できます。 + +```ruby-repl:1 +irb(main):001:0> greeter = Proc.new { |name| puts "Hello, #{name}!" } +irb(main):002:0> greeter +=> # +``` + +作成した `Proc` オブジェクトは、`call` メソッドを使って実行できます。 + +```ruby-repl:2 +irb(main):003:0> greeter.call("Alice") +Hello, Alice! +=> nil +irb(main):004:0> greeter.call("Bob") +Hello, Bob! +=> nil +``` + +`proc` という `Proc.new` のエイリアスメソッドもよく使われます。 + +```ruby-repl:3 +irb(main):005:0> multiplier = proc { |x| x * 2 } +=> # +irb(main):006:0> multiplier.call(10) +=> 20 +``` + +## Proc.new と lambda の違い + +`Proc` オブジェクトを作成するもう一つの方法として `lambda` があります。(`->` というリテラル構文もよく使われます) + +```ruby-repl:4 +irb(main):007:0> adder_lambda = lambda { |a, b| a + b } +=> # +irb(main):008:0> adder_lambda.call(3, 4) +=> 7 + +irb(main):009:0> subtractor_lambda = ->(x, y) { x - y } +=> # +irb(main):010:0> subtractor_lambda.call(10, 3) +=> 7 +``` + +`lambda` で作成されたオブジェクトも `Proc` クラスのインスタンスですが、`Proc.new` (または `proc`) で作成されたものとは、主に以下の2点で挙動が異なります。 + +### 1\. return の挙動 + + * **Proc.new (proc)**: `return` は、Procが定義されたスコープ(通常はメソッド)からリターンします(**ローカルリターン**)。 + * **lambda**: `return` は、`lambda` ブロックの実行からリターンするだけです(**Procからのリターン**)。 + +これは、メソッド内で `Proc` オブジェクトを定義して実行すると、その違いが明確になります。 + +**Proc.new の例:** + +```ruby:proc_return_example.rb +def proc_return_test + # Proc.new で Proc オブジェクトを作成 + my_proc = Proc.new do + puts "Proc: Inside proc" + return "Proc: Returned from proc" # メソッド全体からリターンする + end + + my_proc.call # Proc を実行 + puts "Proc: After proc.call (This will not be printed)" + return "Proc: Returned from method" +end + +puts proc_return_test +``` + +```ruby-exec:proc_return_example.rb +Proc: Inside proc +Proc: Returned from proc +``` + +`proc_return_test` メソッド内の `my_proc.call` が実行された時点で、Proc内の `return` が呼ばれ、メソッド自体が終了していることがわかります。 + +**lambda の例:** + +```ruby:lambda_return_example.rb +def lambda_return_test + # lambda で Proc オブジェクトを作成 + my_lambda = lambda do + puts "Lambda: Inside lambda" + return "Lambda: Returned from lambda" # lambda からリターンするだけ + end + + result = my_lambda.call # lambda を実行 + puts "Lambda: After lambda.call" + puts "Lambda: Result from lambda: #{result}" + return "Lambda: Returned from method" +end + +puts lambda_return_test +``` + +```ruby-exec:lambda_return_example.rb +Lambda: Inside lambda +Lambda: After lambda.call +Lambda: Result from lambda: Lambda: Returned from lambda +Lambda: Returned from method +``` + +`lambda` の場合、`my_lambda.call` 内の `return` は `lambda` の実行を終了させ、その戻り値が `result` 変数に代入されます。メソッドの実行は継続します。 + +### 2\. 引数の厳密さ + + * **Proc.new (proc)**: 引数の数に寛容です。足りない引数は `nil` になり、余分な引数は無視されます。 + * **lambda**: 引数の数を厳密にチェックします。過不足があると `ArgumentError` が発生します。 + +**Proc.new の例:** + +```ruby-repl:5 +irb(main):001:0> my_proc = proc { |a, b| puts "a: #{a.inspect}, b: #{b.inspect}" } +=> # + +irb(main):002:0> my_proc.call(1) # 引数が足りない +a: 1, b: nil +=> nil +irb(main):003:0> my_proc.call(1, 2, 3) # 引数が多い +a: 1, b: 2 +=> nil +``` + +**lambda の例:** + +```ruby-repl:6 +irb(main):004:0> my_lambda = lambda { |a, b| puts "a: #{a.inspect}, b: #{b.inspect}" } +=> # + +irb(main):005:0> my_lambda.call(1) # 引数が足りない +(irb):5:in `block in
': wrong number of arguments (given 1, expected 2) (ArgumentError) + from (irb):5:in `call' + from (irb):5:in `
' +... +irb(main):006:0> my_lambda.call(1, 2, 3) # 引数が多い +(irb):6:in `block in
': wrong number of arguments (given 3, expected 2) (ArgumentError) + from (irb):6:in `call' + from (irb):6:in `
' +... +``` + +一般的に、`lambda` の方が通常のメソッド定義に近い(引数が厳密で、`return` がブロックから抜けるだけ)挙動をするため、使い分けが重要です。 + +## & 演算子の役割 + +`&` 演算子は、ブロックと `Proc` オブジェクトを相互に変換する役割を果たします。 + +### 1\. ブロックを Proc として受け取る + +メソッド定義の最後の引数に `&` をつけて引数名(慣習的に `block`)を指定すると、そのメソッド呼び出し時に渡されたブロックが `Proc` オブジェクトに変換され、その変数に束縛されます。 + +```ruby:block_receiver.rb +# &block でブロックを受け取り、Proc オブジェクトとして扱う +def custom_iterator(items, &block) + puts "Got a Proc object: #{block.inspect}" + + # Proc オブジェクトを call で実行 + items.each do |item| + block.call(item.upcase) # Proc を実行 + end +end + +fruits = ["apple", "banana"] + +# ブロックを渡してメソッドを呼び出す +custom_iterator(fruits) do |fruit| + puts "Processing: #{fruit}" +end +``` + +```ruby-exec:block_receiver.rb +Got a Proc object: # +Processing: APPLE +Processing: BANANA +``` + +これにより、受け取ったブロック(`Proc`)を、メソッド内で好きなタイミングで実行したり、他のメソッドに渡したりすることが可能になります。 + +### 2\. Proc をブロックとして渡す + +逆に、メソッドを呼び出す際に、`Proc` オブジェクトを `&` 付きで渡すと、その `Proc` オブジェクトがブロックとしてメソッドに渡されます。 + +`Array#map` メソッドは通常ブロックを受け取りますが、`Proc` オブジェクトを `&` を使って渡すことができます。 + +```ruby-repl:7 +irb(main):001:0> numbers = [1, 2, 3, 4, 5] +=> [1, 2, 3, 4, 5] + +irb(main):002:0> # 2倍にする Proc オブジェクト +irb(main):003:0> doubler = proc { |n| n * 2 } +=> # + +irb(main):004:0> # & を使って Proc をブロックとして map メソッドに渡す +irb(main):005:0> numbers.map(&doubler) +=> [2, 4, 6, 8, 10] +``` + +これは、以下のコードと等価です。 + +```ruby-repl:8 +irb(main):006:0> numbers.map { |n| n * 2 } +=> [2, 4, 6, 8, 10] +``` + +`&` は、`Proc` とブロック(メソッド呼び出しに付随するコード)の間の架け橋となる重要な演算子です。 + +## クロージャ(スコープ)の概念 + +`Proc` オブジェクト(`lambda` も含む)の非常に重要な特性として、**クロージャ (Closure)** があります。 + +クロージャとは、**Proc オブジェクトが、それが定義された時点のスコープ(環境)を記憶し、後で実行される際にもそのスコープ内の変数(ローカル変数など)にアクセスできる**仕組みです。 + +JavaScriptなど、他の言語でクロージャに触れたことがあるかもしれません。Rubyの `Proc` も同様の機能を提供します。 + +```ruby:closure_example.rb +def counter_generator(initial_value) + count = initial_value + + # この lambda は、外側のスコープにある `count` 変数を記憶する + incrementer = lambda do + puts "Current count: #{count}" + count += 1 # 記憶した変数を更新 + puts "New count: #{count}" + end + + return incrementer # Proc オブジェクトを返す +end + +# counter_generator メソッドの実行は終了し、 +# 本来ローカル変数 `count` は消えるはず... +counter1 = counter_generator(10) + +puts "--- First call ---" +counter1.call # => Current count: 10, New count: 11 + +puts "--- Second call ---" +counter1.call # => Current count: 11, New count: 12 + +# 別のスコープを持つカウンターを作成 +counter2 = counter_generator(100) +puts "--- Counter 2 call ---" +counter2.call # => Current count: 100, New count: 101 + +puts "--- Counter 1 call again ---" +counter1.call # => Current count: 12, New count: 13 +``` + +```ruby-exec:closure_example.rb +--- First call --- +Current count: 10 +New count: 11 +--- Second call --- +Current count: 11 +New count: 12 +--- Counter 2 call --- +Current count: 100 +New count: 101 +--- Counter 1 call again --- +Current count: 12 +New count: 13 +``` + +`counter_generator` メソッドが終了した後でも、返された `lambda` オブジェクト(`counter1` や `counter2`)は、それぞれが定義された時点の `count` 変数を保持し続け、`call` されるたびにそれを更新できます。これがクロージャの力です。 + +## ☕ この章のまとめ + + * **Proc**: ブロックをオブジェクト化したもので、`Proc.new` や `proc` で作成できます。 + * **Lambda**: `lambda` または `->` で作成できる `Proc` オブジェクトの一種です。 + * **Proc と Lambda の違い**: + * **return**: `proc` はローカルリターン、`lambda` はProcからのリターン。 + * **引数**: `proc` は寛容、`lambda` は厳密。 + * **& 演算子**: メソッド定義で使うとブロックを `Proc` として受け取り、メソッド呼び出しで使うと `Proc` をブロックとして渡します。 + * **クロージャ**: `Proc` や `lambda` は、定義された時点のスコープ(ローカル変数など)を記憶し、後からでもアクセスできます。 + +### 練習問題1: Lambda の作成 + +引数を2つ取り、その和を返す `lambda` を作成し、`adder` という変数に代入してください。その後、`adder.call(5, 7)` を実行して `12` が返ってくることを確認してください。 + +```ruby:practice9_1.rb +adder = + +puts adder.call(5, 7) +``` + +```ruby-exec:practice9_1.rb +12 +``` + +### 練習問題2: & を使ったメソッド +数値の配列 `numbers` と、`Proc` オブジェクト `processor` を引数として受け取る `apply_proc_to_array` メソッドを定義してください。メソッド内では、配列の各要素に対して `processor` を実行し、その結果を標準出力に出力するようにしてください。 +(ヒント: メソッド呼び出し側では `&` を使って `Proc` をブロックとして渡すか、メソッド定義側で `&` を使ってブロックを受け取るか、両方の方法が考えられます。ここでは `Proc` オブジェクトをそのまま引数として受け取り、`call` で実行してみてください。) + +```ruby:practice9_2.rb + +``` + +```ruby-exec:practice9_2.rb +``` + diff --git a/public/pyodide.worker.js b/public/pyodide.worker.js index 6135577..43ba76f 100644 --- a/public/pyodide.worker.js +++ b/public/pyodide.worker.js @@ -174,6 +174,15 @@ async function checkSyntax(id, payload) { return; } + // 複数行コマンドは最後に空行を入れないと完了しないものとする + if (code.includes("\n") && code.split("\n").at(-1) !== "") { + self.postMessage({ + id, + payload: { status: "incomplete" }, + }); + return; + } + try { // Pythonのコードを実行して結果を受け取る const status = pyodide.runPython(CHECK_SYNTAX_CODE)(code); @@ -242,4 +251,4 @@ def __execfile(filepath): exec(compile(file.read(), filepath, 'exec'), exec_globals) __execfile -`; \ No newline at end of file +`; diff --git a/public/ruby.worker.js b/public/ruby.worker.js index 94271a4..4c84730 100644 --- a/public/ruby.worker.js +++ b/public/ruby.worker.js @@ -92,12 +92,24 @@ function flushOutput() { stderrBuffer = ""; } -function formatRubyError(error) { +function formatRubyError(error, isFile) { if (!(error instanceof Error)) { return `予期せぬエラー: ${String(error).trim()}`; } - return error.message; + let errorMessage = error.message; + + // Clean up Ruby error messages by filtering out internal eval lines + if (errorMessage.includes("Traceback") || errorMessage.includes("Error")) { + let lines = errorMessage.split("\n"); + lines = lines.filter((line) => line !== "-e:in 'Kernel.eval'"); + if (isFile) { + lines = lines.filter((line) => !line.startsWith("eval:1:in")); + } + errorMessage = lines.join("\n"); + } + + return errorMessage; } async function runCode(id, payload) { @@ -118,22 +130,20 @@ async function runCode(id, payload) { // Flush any buffered output flushOutput(); - const resultStr = result.toString(); + const resultStr = await result.callAsync("inspect"); // Add result to output if it's not nil and not empty - if (resultStr !== "" && resultStr !== "nil") { - rubyOutput.push({ - type: "return", - message: resultStr, - }); - } + rubyOutput.push({ + type: "return", + message: resultStr, + }); } catch (e) { console.log(e); flushOutput(); rubyOutput.push({ type: "error", - message: formatRubyError(e), + message: formatRubyError(e, false), }); } @@ -164,18 +174,16 @@ async function runFile(id, payload) { for (const [filename, content] of Object.entries(files)) { if (content) { rubyVM.eval( - `File.write(${JSON.stringify(filename)}, ${JSON.stringify(content)})` + `File.write(${JSON.stringify(filename)}, ${JSON.stringify(content).replaceAll("#", "\\#")})` ); } } - // Run the specified file - const fileContent = files[name]; - if (!fileContent) { - throw new Error(`File not found: ${name}`); - } + // clear LOADED_FEATURES so that `require` can reload files + rubyVM.eval(`$LOADED_FEATURES.reject! { |f| f =~ /^\\/[^\\/]*\\.rb$/ }`); - rubyVM.eval(fileContent); + // Run the specified file + rubyVM.eval(`load ${JSON.stringify(name)}`); // Flush any buffered output flushOutput(); @@ -185,7 +193,7 @@ async function runFile(id, payload) { rubyOutput.push({ type: "error", - message: formatRubyError(e), + message: formatRubyError(e, true), }); } @@ -216,29 +224,39 @@ async function checkSyntax(id, payload) { // We'll use a simple heuristic const trimmed = code.trim(); - // Check for incomplete syntax patterns - const incompletePatterns = [ - /\bif\b.*(? pattern.test(trimmed))) { - self.postMessage({ id, payload: { status: "incomplete" } }); - return; - } + // // Check for incomplete syntax patterns + // const incompletePatterns = [ + // /\bif\b.*(? pattern.test(trimmed))) { + // self.postMessage({ id, payload: { status: "incomplete" } }); + // return; + // } // Try to compile/evaluate in check mode try { rubyVM.eval(`BEGIN { raise "check" }; ${code}`); } catch (e) { + if ( + e.message && + (e.message.includes("unexpected end-of-input") || // for `if` etc. + e.message.includes("expected a matching") || // for ( ), [ ] + e.message.includes("expected a `}` to close the hash literal") || + e.message.includes("unterminated string meets end of file")) + ) { + self.postMessage({ id, payload: { status: "incomplete" } }); + return; + } // If it's our check exception, syntax is valid if (e.message && e.message.includes("check")) { self.postMessage({ id, payload: { status: "complete" } }); @@ -248,7 +266,6 @@ async function checkSyntax(id, payload) { self.postMessage({ id, payload: { status: "invalid" } }); return; } - self.postMessage({ id, payload: { status: "complete" } }); } catch (e) { console.error("Syntax check error:", e); @@ -338,4 +355,4 @@ self.onmessage = async (event) => { console.error(`Unknown message type: ${type}`); return; } -}; \ No newline at end of file +};