-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathoption.cs
317 lines (292 loc) · 13.4 KB
/
option.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
// =====================================================================================
// NOTICE: To anybody who encounters this code, PLEASE code-review and provide feedback!
// Suggestions as well as any missing features are highly appreciated!
// Thank you in advance!
// =====================================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// static import of Option<T> and OptionExtensions to make it easier to use, e.g. Some(T) instead of Option.Some(T)
using static Optional.Option;
using static Optional.OptionExtensions;
// Option<T> ressembles Rust's Option<T> type.
// It is a generic type that can either hold a value of type T or no value at all.
// Supporting usage:
// - Some(T value) creates an Option<T> with a value.
// - None creates an Option<T> with no value.
// - Match(Func<T, TResult> some, Func<TResult> none) executes the appropriate function based on the Option's value.
// - Map(Func<T, TResult> map) maps the Option's value to a new value.
// - Flatten() flattens an Option<Option<T>> to an Option<T>.
// - Unwrap() unwraps the Option's value (very biased to Rust, do I need Bind() if I have Unwrap()?)
// Example:
// Option<int> some = Some(42);
// Option<int> none = None;
// some.Match( value => Console.WriteLine(value), () => Console.WriteLine("No value"));
// some.Map( value => value * 2).Match( value => Console.WriteLine(value), () => Console.WriteLine("No value"));
// some.Flatten().Match( value => Console.WriteLine(value), () => Console.WriteLine("No value"));
// var v = some.Unwrap();
// var vs = new Option<int>[] { Some(1), None, Some(2) };
// foreach(var v in vs.Flatten()) {...}
// foreach(var v in vs.SelectMany( value => Some(value * 2))) {...}
// foreach(var v in vs.SelectMany( value => Some(value * 2).Flatten())) {...}
namespace Optional
{
public class Option<T> : IEnumerable<T>
{
// making T nullable (via `T?`) would disallow `Option<DateTime>` which is non-nullable type,
// so we'll use a flag (_hasValue) to indicate whether the value is present or not.
private T _value = default(T); // Possible null reference assignment.
private bool _hasValue = false; // always default as None()
// to match Rust's is_some() and is_none()
public bool IsSome() => _hasValue;
public bool IsNone() => !_hasValue;
public T GetValue()
{
// panic if no value
if (IsSome() == false)
{
throw new InvalidOperationException("Option does not have a value");
}
return _value;
}
//
private Option(T init_value)
{
// panic if value is null
if (init_value == null) // Q:what if T is Option<U>? A:it's not null, so it's okay...
{
throw new ArgumentNullException("Option value cannot be null, use None() instead");
}
this._hasValue = true;
this._value = init_value;
}
private Option()
{
_hasValue = false;
_value = default(T); // Possible null reference assignment
}
public static Option<T> Some(T new_value)
{
// should panic if new_value is null
return new Option<T>(new_value);
}
public static Option<T> None
{
get { return new Option<T>(); }
}
// Match() should return TResult where TResult can be void... For void, we need to override
public TResult Match<TResult>(Func<T, TResult> fn_some, Func<TResult> fn_none)
{
return IsSome() ? fn_some(_value) : fn_none();
}
public void Match(Action<T> fn_some, Action fn_none) // explicit override for void
{
// nothing to return...
if (IsSome())
{
fn_some(_value);
}
else
{
fn_none();
}
}
public Option<TResult> Map<TResult>(Func<T, TResult> fn_map)
{
return IsSome() ? Option<TResult>.Some(fn_map(_value)) : Option<TResult>.None;
}
/// <summary>
/// Flattens Option<T> in which if T is of collection type including Option<U>,
/// which is iterable, it'll return value T. For example:
/// - Option<Option<int>> => Option<int> IF Option<int> has a value, else None
/// - Option<Array<int>> => Array<int> IF Array<int> has 1 or more elements, else None
/// - Option<List<int>> => List<int> IF List<int> has 1 or more elements, else None
/// If T is not iterable, Flatten() will return None.
/// </summary>
/// <returns>Some<T> if T has value, else None</returns>
public Option<T> Flatten()
{
//return IsSome() ? _value as Option<T> : Option<T>.None;
if (IsSome())
{
if (_value is IEnumerable<T>)
{
var enumerable = _value as IEnumerable<T>;
if (enumerable.Any())
{
return Some(_value);
}
else
{
return None;
}
}
else
{
return Some(_value);
}
}
else
{
return None;
}
}
public T Unwrap()
{
if (IsSome())
{
return _value;
}
else
{
throw new InvalidOperationException("Option does not have a value");
}
}
/// <summary>
/// Derivation of IEnumerable<T>, so mapping and flattening works
/// Option<T> as enumerable collection is treated as if it's a collection of T
/// of either 0 or 1 element in the collection/list/array
/// </summary>
/// <returns></returns>
public IEnumerator<T> GetEnumerator()
{
if (IsSome())
{
yield return _value;
}
}
/// <summary>
/// Derivation of IEnumerable<T>, so mapping and flattening works
/// Option<T> as enumerable collection is treated as if it's a collection of T
/// of either 0 or 1 element in the collection/list/array
/// </summary>
/// <returns></returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public static class OptionExtensions
{
public static IEnumerable<T> Flatten<T>(this IEnumerable<Option<T>> options)
{
foreach (var option in options)
{
if (option.IsSome())
{
yield return option.GetValue();
}
}
}
public static Option<TResult> SelectMany<T, TResult>(this Option<T> option, Func<T, Option<TResult>> fn_map)
{
if (option.IsSome())
{
return fn_map(option.GetValue());
}
else
{
return Option<TResult>.None;
}
}
// TODO: override ToString() (similar to Rust's Debug and Display traits) to print out Some(T) or None
//public override string ToString() => throw new NotImplementedException();
}
// I want to just express it as Some(T) instead of Option.Some(T):
public static class Option
{
public static Option<T> Some<T>(T value) => Option<T>.Some(value);
public static Option<T> None<T>() => Option<T>.None;
}
// for unit-test'ish code to test out (demonstrate) Option<T> usages and patterns:
class OptionTest
{
// Minor explanation: I have a habbit of declaring my variables as "possible_X" (yes, like Hungarian notation, and yes,
// my variables are Rust-like snake_case) to indicate that it's an Option<T> type. This is just a personal preference.
public static void Test()
{
// note that we're using `using static Optional.Option;` and `using static Optional.OptionExtensions;` so that
// we can use Some(T) and None<T> directly without Option.Some(T) and Option.None<T>
Option<int> possible_int = Some(42);
Option<int> possible_int2 = None<int>(); // TODO: The explicity variable declartion should not require None<int>() to be explicit (should be able to do None() here?)
// type inference
var possible_type_inferred_value = Some(42);
var possible_type_inferred_value_for_none = None<bool>(); // you MUST specify the type for None() to work (obvious, but )
var possible_tuple = Some((answer: 42, motto: "Don't Panic!", favor_vogon_poems: false));
Console.WriteLine($"Type inference: should print '42' here: {possible_type_inferred_value.Match(value => value, () => -1)}");
Console.WriteLine($"Possible_Tuple: Answer={possible_tuple.Match(value => value.answer, () => -1)}, Motto={possible_tuple.Match(value => value.motto, () => "No value")}, Favor Vogon Poems={possible_tuple.Match(value => value.favor_vogon_poems, () => false)} ");
// using Select() to iterate over an Option<T> as if it was a collection
Console.WriteLine("Select() over Option<T>: should print '42' here");
foreach(var v in possible_type_inferred_value.Select(value => value))
{
Console.WriteLine(v);
}
// iterating over a None<T> should not execute the lambda body of foreach()
Console.WriteLine("Select() over None<T>: should NOT throw exception... The beauty of no more NullException!!!!");
foreach(var v in possible_type_inferred_value_for_none.Select(value => value))
{
throw new Exception("This should not be reached");
}
// pattern matching (void return type and non-void return type)
Console.WriteLine("Pattern matching: should print '42' here");
possible_int.Match(value => Console.WriteLine(value), () => Console.WriteLine("No value"));
var match_result = possible_int.Match(value => value, () => -1); // pattern matching (non-void return type)
// mapping
Console.WriteLine("Mapping: should print '84' (2 * 42) here");
possible_int.Map(value => value * 2)
.Match(
value => Console.WriteLine(value), // Some
() => Console.WriteLine("No value")); // None
// flattening
Console.WriteLine("Flattening: should print '42' here");
possible_int.Flatten()
.Match(
value => Console.WriteLine(value), // Some
() => Console.WriteLine("No value")); // None
// unwrapping
Console.WriteLine("Unwrapping: should print '42' here");
var v_some = possible_int.Unwrap(); // will throw if it is none
Console.WriteLine(v_some);
Console.WriteLine("Unwrapping: should print 'Option does not have a value' here (exception InvalidOperationException thrown)");
try
{
var v_none = possible_int2.Unwrap(); // this SHOULD throw
}
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message);
}
// enumeration
var vs = new Option<int>[] { Some(1), None<int>(), Some(2) };
// declaring `int` explicitly instead of `var v` to demonstrate expected type, but usually you want to do `foreach(var v...)`
Console.WriteLine("Enumeration: should print '1' and '2' here");
foreach (int v_int in vs.Flatten())
{
Console.WriteLine(v_int);
}
// Note the type inferences on Select<Option<int>, Option<int>> here and Select() iterates over the Option<int>
Console.WriteLine("Enumeration (collection of Option<T>): should print '2 (mapped)'; 'No value (mapped)'; '4 (mapped)' here");
foreach (Option<int> possible_v_int in vs.Select(possible_val_t => possible_val_t.Match(val_t => Some(val_t * 2), () => None<int>())))
{
Console.WriteLine(possible_v_int.Match(
value => value.ToString(),
() => "No value") + " (mapped)");
}
// Similar to above, but here, we call Flatten() so that Select() iterates over the int
Console.WriteLine("Enumeration (collection flattened Select() results): should print '2' and '4' here");
foreach (int v_int in vs.Select(possible_val_t => possible_val_t.Match(val_t => Some(val_t * 2), () => None<int>())).Flatten())
{
Console.WriteLine(v_int);
}
// Using SelectMany() to flatten the implicitly
Console.WriteLine("Enumeration (flattened using SelctMany): should print '2' and '4' here (SelectMany())");
foreach (int v_int in vs.SelectMany(possible_val_t => possible_val_t.Match(val_t => Some(val_t * 2), () => None<int>())))
{
Console.WriteLine(v_int);
}
}
}
}