Skip to content

Commit f51aa44

Browse files
committed
blog: add keyword arguments tutorial post
Add comprehensive blog post explaining T-Ruby's keyword argument syntax with three language versions (EN, KO, JA). - Explain syntax collision between type annotations and Ruby keywords - Document three patterns: { }, variable: { }, and **opts - Include interface usage examples with explicit field names - Add design history section with Issue #19 context
1 parent b146291 commit f51aa44

File tree

3 files changed

+810
-0
lines changed
  • blog/2025-12-29-keyword-arguments
  • i18n
    • ja/docusaurus-plugin-content-blog/2025-12-29-keyword-arguments
    • ko/docusaurus-plugin-content-blog/2025-12-29-keyword-arguments

3 files changed

+810
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
---
2+
slug: keyword-arguments-type-definitions
3+
title: "Handling Keyword Arguments in T-Ruby"
4+
authors: [yhk1038]
5+
tags: [tutorial, syntax, keyword-arguments]
6+
---
7+
8+
When we first released T-Ruby, one of the most frequently asked questions was: **"How do I define keyword arguments?"** — this was [Issue #19](https://github.com/aspect-build/t-ruby/issues/19) - and it turned out to be one of the most important design decisions for the language.
9+
10+
<!-- truncate -->
11+
12+
## The Problem: Syntax Collision
13+
14+
In T-Ruby, type annotations use the colon syntax: `name: Type`. But Ruby's keyword arguments also use a colon: `name: value`. This creates a fundamental conflict.
15+
16+
Consider this T-Ruby code:
17+
18+
```ruby
19+
def foo(x: String, y: Integer = 10)
20+
```
21+
22+
Is `x` a keyword argument or a positional argument with a type annotation? In early T-Ruby, this was always treated as a **positional argument** - you'd call it as `foo("hi", 20)`.
23+
24+
But what if you wanted actual keyword arguments that you call as `foo(x: "hi", y: 20)`?
25+
26+
## The Solution: A Simple Rule
27+
28+
T-Ruby solves this with one elegant rule: **the presence of a variable name determines the meaning**.
29+
30+
| Syntax | Meaning | Compiles To |
31+
|--------|---------|-------------|
32+
| `{ name: String }` | Keyword argument (destructuring) | `def foo(name:)` |
33+
| `config: { host: String }` | Hash literal parameter | `def foo(config)` |
34+
| `**opts: Type` | Double splat for forwarding | `def foo(**opts)` |
35+
36+
Let's explore each pattern.
37+
38+
## Pattern 1: Keyword Arguments with `{ }`
39+
40+
When you use curly braces **without a variable name**, T-Ruby treats it as keyword argument destructuring:
41+
42+
```ruby
43+
# T-Ruby
44+
def greet({ name: String, prefix: String = "Hello" }): String
45+
"#{prefix}, #{name}!"
46+
end
47+
48+
# How to call it
49+
greet(name: "Alice")
50+
greet(name: "Bob", prefix: "Hi")
51+
```
52+
53+
This compiles to:
54+
55+
```ruby
56+
# Ruby
57+
def greet(name:, prefix: "Hello")
58+
"#{prefix}, #{name}!"
59+
end
60+
```
61+
62+
And generates this RBS signature:
63+
64+
```rbs
65+
def greet: (name: String, ?prefix: String) -> String
66+
```
67+
68+
### Key Points
69+
70+
- Wrap keyword arguments in `{ }`
71+
- Each argument has a type: `name: String`
72+
- Default values work naturally: `prefix: String = "Hello"`
73+
- The `?` in RBS indicates optional parameters
74+
75+
## Pattern 2: Hash Literal with Variable Name
76+
77+
When you add a variable name before the braces, T-Ruby treats it as a Hash parameter:
78+
79+
```ruby
80+
# T-Ruby
81+
def process(config: { host: String, port: Integer }): String
82+
"#{config[:host]}:#{config[:port]}"
83+
end
84+
85+
# How to call it
86+
process(config: { host: "localhost", port: 8080 })
87+
```
88+
89+
This compiles to:
90+
91+
```ruby
92+
# Ruby
93+
def process(config)
94+
"#{config[:host]}:#{config[:port]}"
95+
end
96+
```
97+
98+
Use this pattern when:
99+
- You want to pass an entire Hash object
100+
- You need to access values with `config[:key]` syntax
101+
- The Hash might be stored or passed to other methods
102+
103+
## Pattern 3: Double Splat with `**`
104+
105+
For collecting arbitrary keyword arguments or forwarding them to other methods:
106+
107+
```ruby
108+
# T-Ruby
109+
def with_transaction(**config: DbConfig): String
110+
conn = connect_db(**config)
111+
"BEGIN; #{conn}; COMMIT;"
112+
end
113+
```
114+
115+
This compiles to:
116+
117+
```ruby
118+
# Ruby
119+
def with_transaction(**config)
120+
conn = connect_db(**config)
121+
"BEGIN; #{conn}; COMMIT;"
122+
end
123+
```
124+
125+
The `**` is preserved because Ruby's `opts: Type` compiles to `def foo(opts:)` (a single keyword argument named `opts`), not `def foo(**opts)` (collecting all keyword arguments).
126+
127+
## Mixing Positional and Keyword Arguments
128+
129+
You can combine positional arguments with keyword arguments:
130+
131+
```ruby
132+
# T-Ruby
133+
def mixed(id: Integer, { name: String, age: Integer = 0 }): String
134+
"#{id}: #{name} (#{age})"
135+
end
136+
137+
# How to call it
138+
mixed(1, name: "Alice")
139+
mixed(2, name: "Bob", age: 30)
140+
```
141+
142+
Compiles to:
143+
144+
```ruby
145+
# Ruby
146+
def mixed(id, name:, age: 0)
147+
"#{id}: #{name} (#{age})"
148+
end
149+
```
150+
151+
## Using Interfaces
152+
153+
For complex configurations, define an interface and reference it:
154+
155+
```ruby
156+
# Define the interface
157+
interface ConnectionOptions
158+
host: String
159+
port?: Integer
160+
timeout?: Integer
161+
end
162+
163+
# Destructuring with interface reference - specify field names with defaults
164+
def connect({ host:, port: 8080, timeout: 30 }: ConnectionOptions): String
165+
"#{host}:#{port}"
166+
end
167+
168+
# How to call it
169+
connect(host: "localhost")
170+
connect(host: "localhost", port: 3000)
171+
172+
# Double splat - for forwarding keyword arguments
173+
def forward(**opts: ConnectionOptions): String
174+
connect(**opts)
175+
end
176+
```
177+
178+
Note that when using interface references, you must explicitly list the field names in the destructuring pattern. Default values are specified in the function definition, not in the interface.
179+
180+
## Complete Example
181+
182+
Here's a real-world example combining multiple patterns:
183+
184+
```ruby
185+
# T-Ruby
186+
class ApiClient
187+
def initialize({ base_url: String, timeout: Integer = 30 })
188+
@base_url = base_url
189+
@timeout = timeout
190+
end
191+
192+
def get({ path: String }): String
193+
"#{@base_url}#{path}"
194+
end
195+
196+
def post(path: String, { body: String, headers: Hash = {} }): String
197+
"POST #{@base_url}#{path}"
198+
end
199+
end
200+
201+
# Usage
202+
client = ApiClient.new(base_url: "https://api.example.com")
203+
client.get(path: "/users")
204+
client.post("/users", body: "{}", headers: { "Content-Type" => "application/json" })
205+
```
206+
207+
This compiles to:
208+
209+
```ruby
210+
# Ruby
211+
class ApiClient
212+
def initialize(base_url:, timeout: 30)
213+
@base_url = base_url
214+
@timeout = timeout
215+
end
216+
217+
def get(path:)
218+
"#{@base_url}#{path}"
219+
end
220+
221+
def post(path, body:, headers: {})
222+
"POST #{@base_url}#{path}"
223+
end
224+
end
225+
```
226+
227+
## Quick Reference
228+
229+
| What You Want | T-Ruby Syntax | Ruby Output |
230+
|---------------|---------------|-------------|
231+
| Required keyword arg | `{ name: String }` | `name:` |
232+
| Optional keyword arg | `{ name: String = "default" }` | `name: "default"` |
233+
| Multiple keyword args | `{ a: String, b: Integer }` | `a:, b:` |
234+
| Hash parameter | `opts: { a: String }` | `opts` |
235+
| Double splat | `**opts: Type` | `**opts` |
236+
| Mixed | `id: Integer, { name: String }` | `id, name:` |
237+
238+
## Design History
239+
240+
When we first announced T-Ruby, the initial syntax used `**{}` for keyword arguments:
241+
242+
```ruby
243+
# Initial design (rejected)
244+
def greet(**{ name: String, prefix: String = "Hello" }): String
245+
```
246+
247+
Community feedback pointed out this was too complex. We explored several alternatives:
248+
249+
| Alternative | Example | Result |
250+
|-------------|---------|--------|
251+
| Semicolon | `; name: String` | Rejected (worse readability) |
252+
| Double colon | `name:: String` | Rejected (`::` conflicts with Ruby constants) |
253+
| `named` keyword | `named name: String` | Considered |
254+
| **Braces only** | `{ name: String }` | **Adopted** |
255+
256+
The final design uses a simple rule: the presence of a variable name determines the meaning. This creates a clean, intuitive syntax that doesn't require new keywords.
257+
258+
## Summary
259+
260+
T-Ruby's keyword argument syntax is designed to be intuitive:
261+
262+
1. **Wrap in `{ }`** for keyword arguments
263+
2. **Add a variable name** for Hash parameters
264+
3. **Use `**`** for double splat forwarding
265+
266+
This simple rule eliminates the confusion between type annotations and Ruby keyword syntax, giving you the best of both worlds: TypeScript-style type safety with Ruby's expressive keyword arguments.
267+
268+
---
269+
270+
*Keyword argument support is available in T-Ruby v0.0.41 and later. Try it out and let us know what you think!*

0 commit comments

Comments
 (0)