Design a result-returning API
Suppose a program divides a quantity among teams, then divides each team’s share among its members. We want whole-number shares and a useful explanation when the division is invalid.
For 120 items, 3 teams, and 4 people per team, each person receives 10. A zero team count or an uneven split should produce an error value that the caller can inspect.
1. Define one operation’s contract
Section titled “1. Define one operation’s contract”Start with divide_evenly(total, groups). It accepts two integers and succeeds only when:
groupsis positive;totalis nonnegative;total % groupsis zero.
The order matters. Check the divisor before evaluating remainder or division. A zero divisor must be rejected before the arithmetic runs.
KataScript integer division truncates. Without the remainder check, 100 / 3 would quietly produce a whole-number quotient while leaving an unaccounted remainder. The validation gives the operation a stronger meaning than plain division.
2. Return success or failure as data
Section titled “2. Return success or failure as data”The return type is Res[Int, Str]. Its successful variant carries the share; its error variant carries an explanation.
An invalid input returns Res[Int, Str].Err("..."). A valid input returns Res[Int, Str].Val(total / groups). Both are ordinary values that the caller receives without stopping the interpreter.
A string keeps this first API small. For a larger program, an enum such as enum ShareError { InvalidGroups, NegativeTotal, Uneven } would let callers distinguish error categories without comparing message text.
3. Compose two divisions
Section titled “3. Compose two divisions”The per_person function calls divide_evenly twice. The first call divides the total among teams; the second divides a team’s share among people.
Postfix ? unwraps a successful result. On an error, it returns the complete error value from per_person immediately. The later operation does not run.
Both functions return exactly Res[Int, Str]. That is significant: propagation does not convert an error or rebuild a different result instantiation automatically.
4. Render at the boundary
Section titled “4. Render at the boundary”The report function matches on the result and prints a sentence for each case. Arithmetic code returns values; the reporting function decides how those values appear to a person.
func divide_evenly(total: Int, groups: Int): Res[Int, Str] { if groups <= 0 { ret Res[Int, Str].Err("group count must be positive") } if total < 0 { ret Res[Int, Str].Err("total must be nonnegative") } if total % groups != 0 { ret Res[Int, Str].Err("cannot divide evenly") } ret Res[Int, Str].Val(total / groups)}func per_person(total: Int, teams: Int, people: Int): Res[Int, Str] { let per_team = divide_evenly(total, teams)? let share = divide_evenly(per_team, people)? ret Res[Int, Str].Val(share)}func report(result: Res[Int, Str]) { match result { Val(amount) -> print("each person gets {amount}"), Err(reason) -> print("cannot share: {reason}"), }}report(per_person(120, 3, 4))report(per_person(120, 0, 4))report(per_person(100, 3, 4))report(per_person(120, 3, 0))Original example: checked output and syntax tree
These records belong to the original downloadable program. Run the editor above to see results for your changes.
each person gets 10 cannot share: group count must be positive cannot share: cannot divide evenly cannot share: group count must be positive
Original AST
[
{
"node": {
"FuncDef": {
"name": {
"node": "divide_evenly",
"span": [
5,
18
]
},
"params": [
{
"name": {
"node": "total",
"span": [
19,
24
]
},
"type_ann": {
"node": {
"Name": "Int"
},
"span": [
26,
29
]
}
},
{
"name": {
"node": "groups",
"span": [
31,
37
]
},
"type_ann": {
"node": {
"Name": "Int"
},
"span": [
39,
42
]
}
}
],
"ret_type": {
"node": {
"Item": {
"object": {
"node": {
"Name": "Res"
},
"span": [
45,
48
]
},
"args": [
{
"node": {
"Name": "Int"
},
"span": [
49,
52
]
},
{
"node": {
"Name": "Str"
},
"span": [
54,
57
]
}
]
}
},
"span": [
45,
58
]
},
"body": [
{
"node": {
"Expr": {
"node": {
"If": {
"cond": {
"node": {
"BinOp": {
"op": "Le",
"left": {
"node": {
"Name": "groups"
},
"span": [
68,
74
]
},
"right": {
"node": {
"Int": "0"
},
"span": [
78,
79
]
}
}
},
"span": [
68,
79
]
},
"then_body": [
{
"node": {
"Ret": {
"keyword": [
82,
85
],
"value": {
"node": {
"Call": {
"callee": {
"node": {
"Attr": {
"object": {
"node": {
"Item": {
"object": {
"node": {
"Name": "Res"
},
"span": [
86,
89
]
},
"args": [
{
"node": {
"Name": "Int"
},
"span": [
90,
93
]
},
{
"node": {
"Name": "Str"
},
"span": [
95,
98
]
}
]
}
},
"span": [
86,
99
]
},
"name": "Err",
"name_span": [
100,
103
]
}
},
"span": [
86,
103
]
},
"args": [
{
"node": {
"Str": "group count must be positive"
},
"span": [
104,
134
]
}
],
"args_span": [
103,
135
]
}
},
"span": [
86,
135
]
}
}
},
"span": [
82,
135
]
}
],
"else_body": null
}
},
"span": [
65,
137
]
}
},
"span": [
65,
137
]
},
{
"node": {
"Expr": {
"node": {
"If": {
"cond": {
"node": {
"BinOp": {
"op": "Lt",
"left": {
"node": {
"Name": "total"
},
"span": [
145,
150
]
},
"right": {
"node": {
"Int": "0"
},
"span": [
153,
154
]
}
}
},
"span": [
145,
154
]
},
"then_body": [
{
"node": {
"Ret": {
"keyword": [
157,
160
],
"value": {
"node": {
"Call": {
"callee": {
"node": {
"Attr": {
"object": {
"node": {
"Item": {
"object": {
"node": {
"Name": "Res"
},
"span": [
161,
164
]
},
"args": [
{
"node": {
"Name": "Int"
},
"span": [
165,
168
]
},
{
"node": {
"Name": "Str"
},
"span": [
170,
173
]
}
]
}
},
"span": [
161,
174
]
},
"name": "Err",
"name_span": [
175,
178
]
}
},
"span": [
161,
178
]
},
"args": [
{
"node": {
"Str": "total must be nonnegative"
},
"span": [
179,
206
]
}
],
"args_span": [
178,
207
]
}
},
"span": [
161,
207
]
}
}
},
"span": [
157,
207
]
}
],
"else_body": null
}
},
"span": [
142,
209
]
}
},
"span": [
142,
209
]
},
{
"node": {
"Expr": {
"node": {
"If": {
"cond": {
"node": {
"BinOp": {
"op": "Ne",
"left": {
"node": {
"BinOp": {
"op": "Mod",
"left": {
"node": {
"Name": "total"
},
"span": [
217,
222
]
},
"right": {
"node": {
"Name": "groups"
},
"span": [
225,
231
]
}
}
},
"span": [
217,
231
]
},
"right": {
"node": {
"Int": "0"
},
"span": [
235,
236
]
}
}
},
"span": [
217,
236
]
},
"then_body": [
{
"node": {
"Ret": {
"keyword": [
239,
242
],
"value": {
"node": {
"Call": {
"callee": {
"node": {
"Attr": {
"object": {
"node": {
"Item": {
"object": {
"node": {
"Name": "Res"
},
"span": [
243,
246
]
},
"args": [
{
"node": {
"Name": "Int"
},
"span": [
247,
250
]
},
{
"node": {
"Name": "Str"
},
"span": [
252,
255
]
}
]
}
},
"span": [
243,
256
]
},
"name": "Err",
"name_span": [
257,
260
]
}
},
"span": [
243,
260
]
},
"args": [
{
"node": {
"Str": "cannot divide evenly"
},
"span": [
261,
283
]
}
],
"args_span": [
260,
284
]
}
},
"span": [
243,
284
]
}
}
},
"span": [
239,
284
]
}
],
"else_body": null
}
},
"span": [
214,
286
]
}
},
"span": [
214,
286
]
},
{
"node": {
"Ret": {
"keyword": [
291,
294
],
"value": {
"node": {
"Call": {
"callee": {
"node": {
"Attr": {
"object": {
"node": {
"Item": {
"object": {
"node": {
"Name": "Res"
},
"span": [
295,
298
]
},
"args": [
{
"node": {
"Name": "Int"
},
"span": [
299,
302
]
},
{
"node": {
"Name": "Str"
},
"span": [
304,
307
]
}
]
}
},
"span": [
295,
308
]
},
"name": "Val",
"name_span": [
309,
312
]
}
},
"span": [
295,
312
]
},
"args": [
{
"node": {
"BinOp": {
"op": "Div",
"left": {
"node": {
"Name": "total"
},
"span": [
313,
318
]
},
"right": {
"node": {
"Name": "groups"
},
"span": [
321,
327
]
}
}
},
"span": [
313,
327
]
}
],
"args_span": [
312,
328
]
}
},
"span": [
295,
328
]
}
}
},
"span": [
291,
328
]
}
]
}
},
"span": [
0,
330
]
},
{
"node": {
"FuncDef": {
"name": {
"node": "per_person",
"span": [
336,
346
]
},
"params": [
{
"name": {
"node": "total",
"span": [
347,
352
]
},
"type_ann": {
"node": {
"Name": "Int"
},
"span": [
354,
357
]
}
},
{
"name": {
"node": "teams",
"span": [
359,
364
]
},
"type_ann": {
"node": {
"Name": "Int"
},
"span": [
366,
369
]
}
},
{
"name": {
"node": "people",
"span": [
371,
377
]
},
"type_ann": {
"node": {
"Name": "Int"
},
"span": [
379,
382
]
}
}
],
"ret_type": {
"node": {
"Item": {
"object": {
"node": {
"Name": "Res"
},
"span": [
385,
388
]
},
"args": [
{
"node": {
"Name": "Int"
},
"span": [
389,
392
]
},
{
"node": {
"Name": "Str"
},
"span": [
394,
397
]
}
]
}
},
"span": [
385,
398
]
},
"body": [
{
"node": {
"Let": {
"pattern": {
"node": {
"Binding": {
"node": "per_team",
"span": [
409,
417
]
}
},
"span": [
409,
417
]
},
"type_ann": null,
"value": {
"node": {
"Ques": {
"node": {
"Call": {
"callee": {
"node": {
"Name": "divide_evenly"
},
"span": [
420,
433
]
},
"args": [
{
"node": {
"Name": "total"
},
"span": [
434,
439
]
},
{
"node": {
"Name": "teams"
},
"span": [
441,
446
]
}
],
"args_span": [
433,
447
]
}
},
"span": [
420,
447
]
}
},
"span": [
420,
448
]
}
}
},
"span": [
405,
448
]
},
{
"node": {
"Let": {
"pattern": {
"node": {
"Binding": {
"node": "share",
"span": [
457,
462
]
}
},
"span": [
457,
462
]
},
"type_ann": null,
"value": {
"node": {
"Ques": {
"node": {
"Call": {
"callee": {
"node": {
"Name": "divide_evenly"
},
"span": [
465,
478
]
},
"args": [
{
"node": {
"Name": "per_team"
},
"span": [
479,
487
]
},
{
"node": {
"Name": "people"
},
"span": [
489,
495
]
}
],
"args_span": [
478,
496
]
}
},
"span": [
465,
496
]
}
},
"span": [
465,
497
]
}
}
},
"span": [
453,
497
]
},
{
"node": {
"Ret": {
"keyword": [
502,
505
],
"value": {
"node": {
"Call": {
"callee": {
"node": {
"Attr": {
"object": {
"node": {
"Item": {
"object": {
"node": {
"Name": "Res"
},
"span": [
506,
509
]
},
"args": [
{
"node": {
"Name": "Int"
},
"span": [
510,
513
]
},
{
"node": {
"Name": "Str"
},
"span": [
515,
518
]
}
]
}
},
"span": [
506,
519
]
},
"name": "Val",
"name_span": [
520,
523
]
}
},
"span": [
506,
523
]
},
"args": [
{
"node": {
"Name": "share"
},
"span": [
524,
529
]
}
],
"args_span": [
523,
530
]
}
},
"span": [
506,
530
]
}
}
},
"span": [
502,
530
]
}
]
}
},
"span": [
331,
532
]
},
{
"node": {
"FuncDef": {
"name": {
"node": "report",
"span": [
538,
544
]
},
"params": [
{
"name": {
"node": "result",
"span": [
545,
551
]
},
"type_ann": {
"node": {
"Item": {
"object": {
"node": {
"Name": "Res"
},
"span": [
553,
556
]
},
"args": [
{
"node": {
"Name": "Int"
},
"span": [
557,
560
]
},
{
"node": {
"Name": "Str"
},
"span": [
562,
565
]
}
]
}
},
"span": [
553,
566
]
}
}
],
"ret_type": null,
"body": [
{
"node": {
"Expr": {
"node": {
"Match": {
"keyword": [
574,
579
],
"subject": {
"node": {
"Name": "result"
},
"span": [
580,
586
]
},
"arms": [
{
"pattern": {
"node": {
"Variant": {
"name": {
"node": "Val",
"span": [
597,
600
]
},
"bindings": [
{
"node": {
"Binding": {
"node": "amount",
"span": [
601,
607
]
}
},
"span": [
601,
607
]
}
]
}
},
"span": [
597,
608
]
},
"body": [
{
"node": {
"Expr": {
"node": {
"Call": {
"callee": {
"node": {
"Name": "print"
},
"span": [
612,
617
]
},
"args": [
{
"node": {
"Interp": {
"parts": [
{
"Lit": "each person gets "
},
{
"Expr": {
"node": {
"Name": "amount"
},
"span": [
637,
643
]
}
}
]
}
},
"span": [
618,
645
]
}
],
"args_span": [
617,
646
]
}
},
"span": [
612,
646
]
}
},
"span": [
612,
646
]
}
]
},
{
"pattern": {
"node": {
"Variant": {
"name": {
"node": "Err",
"span": [
656,
659
]
},
"bindings": [
{
"node": {
"Binding": {
"node": "reason",
"span": [
660,
666
]
}
},
"span": [
660,
666
]
}
]
}
},
"span": [
656,
667
]
},
"body": [
{
"node": {
"Expr": {
"node": {
"Call": {
"callee": {
"node": {
"Name": "print"
},
"span": [
671,
676
]
},
"args": [
{
"node": {
"Interp": {
"parts": [
{
"Lit": "cannot share: "
},
{
"Expr": {
"node": {
"Name": "reason"
},
"span": [
693,
699
]
}
}
]
}
},
"span": [
677,
701
]
}
],
"args_span": [
676,
702
]
}
},
"span": [
671,
702
]
}
},
"span": [
671,
702
]
}
]
}
]
}
},
"span": [
574,
709
]
}
},
"span": [
574,
709
]
}
]
}
},
"span": [
533,
711
]
},
{
"node": {
"Expr": {
"node": {
"Call": {
"callee": {
"node": {
"Name": "report"
},
"span": [
712,
718
]
},
"args": [
{
"node": {
"Call": {
"callee": {
"node": {
"Name": "per_person"
},
"span": [
719,
729
]
},
"args": [
{
"node": {
"Int": "120"
},
"span": [
730,
733
]
},
{
"node": {
"Int": "3"
},
"span": [
735,
736
]
},
{
"node": {
"Int": "4"
},
"span": [
738,
739
]
}
],
"args_span": [
729,
740
]
}
},
"span": [
719,
740
]
}
],
"args_span": [
718,
741
]
}
},
"span": [
712,
741
]
}
},
"span": [
712,
741
]
},
{
"node": {
"Expr": {
"node": {
"Call": {
"callee": {
"node": {
"Name": "report"
},
"span": [
742,
748
]
},
"args": [
{
"node": {
"Call": {
"callee": {
"node": {
"Name": "per_person"
},
"span": [
749,
759
]
},
"args": [
{
"node": {
"Int": "120"
},
"span": [
760,
763
]
},
{
"node": {
"Int": "0"
},
"span": [
765,
766
]
},
{
"node": {
"Int": "4"
},
"span": [
768,
769
]
}
],
"args_span": [
759,
770
]
}
},
"span": [
749,
770
]
}
],
"args_span": [
748,
771
]
}
},
"span": [
742,
771
]
}
},
"span": [
742,
771
]
},
{
"node": {
"Expr": {
"node": {
"Call": {
"callee": {
"node": {
"Name": "report"
},
"span": [
772,
778
]
},
"args": [
{
"node": {
"Call": {
"callee": {
"node": {
"Name": "per_person"
},
"span": [
779,
789
]
},
"args": [
{
"node": {
"Int": "100"
},
"span": [
790,
793
]
},
{
"node": {
"Int": "3"
},
"span": [
795,
796
]
},
{
"node": {
"Int": "4"
},
"span": [
798,
799
]
}
],
"args_span": [
789,
800
]
}
},
"span": [
779,
800
]
}
],
"args_span": [
778,
801
]
}
},
"span": [
772,
801
]
}
},
"span": [
772,
801
]
},
{
"node": {
"Expr": {
"node": {
"Call": {
"callee": {
"node": {
"Name": "report"
},
"span": [
802,
808
]
},
"args": [
{
"node": {
"Call": {
"callee": {
"node": {
"Name": "per_person"
},
"span": [
809,
819
]
},
"args": [
{
"node": {
"Int": "120"
},
"span": [
820,
823
]
},
{
"node": {
"Int": "3"
},
"span": [
825,
826
]
},
{
"node": {
"Int": "0"
},
"span": [
828,
829
]
}
],
"args_span": [
819,
830
]
}
},
"span": [
809,
830
]
}
],
"args_span": [
808,
831
]
}
},
"span": [
802,
831
]
}
},
"span": [
802,
831
]
}
]The four calls cover success, an invalid first divisor, an uneven first split, and an invalid second divisor. The last case confirms that an error from the second stage travels through the same result path.
5. Check the boundary cases
Section titled “5. Check the boundary cases”Try per_person(0, 3, 4): it should succeed with zero. Zero items are allowed by the contract; zero groups are not.
Try per_person(120, 3, 6): the first split succeeds with 40, but the second split should report that it cannot divide evenly. This tests a failure that occurs only after the first result was unwrapped.
Try a negative total: the function should return the nonnegative-total error before computing a share.
A result is not a catch mechanism
Section titled “A result is not a catch mechanism”? handles an enum value returned by an operation. It does not catch arbitrary runtime errors. For example, invalid Str.to_int() input currently stops execution instead of returning Res, so attaching ? to that conversion does not make it recoverable.
Use match when the caller needs to choose a response, ? when the caller should propagate failure, and ! only when failure should stop execution. See the error guide for those operators together.