Skip to content

Repository files navigation

Rustify

Rustify is a .NET library that brings some of the best features from Rust into the C# world, aiming to provide more robust and expressive ways to handle common programming patterns. This library includes popular Rust constructs like Option<T>, Result<T, E>, Unit, TaggedUnion, Arc<T> (Atomic Reference Counting), and RwLock<T> (Read-Write Lock).

NuGet.NET

Features

Core Types

  • Option<T>: Represents an optional value. It can be Some(value) or None, helping to avoid null reference exceptions and making code more explicit about the possibility of missing values.
  • Result<T, E>: Represents a value that can be either Ok(value) or Err(error). This is useful for error handling without relying on exceptions, making control flow more predictable.
  • Unit: Represents a type with a single value, (). It's often used as a return type for functions that perform an action but don't return a meaningful value, similar to void but can be used as a generic type argument.
  • TaggedUnion<T...>: A type-safe discriminated union that can hold one of several types. Available in 1-ary, 2-ary, and 3-ary variants.

Synchronization Primitives

  • Arc<T> (Atomic Reference Counter): A thread-safe reference-counted pointer. Arc<T> provides shared ownership of a value of type T, allocated on the heap. It ensures that the value is deallocated only when the last Arc pointer to it is dropped.
  • RwLock<T> (Read-Write Lock): A synchronization primitive that allows multiple readers or a single writer at any point in time. RwLock<T> is useful when you have data that is read frequently but written infrequently.
  • GenericMutex<T>: A generic mutex wrapper that provides safe, exclusive access to a value.

Extensions & LINQ Support

  • LINQ Query Syntax: Both Option<T> and Result<T, E> support LINQ query syntax with Select, SelectMany, and Where methods.
  • Async Extensions: MapAsync, AndThenAsync, and UnwrapOrAsync for seamless async/await integration.

Installation

You can install Rustify via NuGet Package Manager:

Install-Package Rustify

Or via .NET CLI:

dotnet add package Rustify

Usage

Option<T>

Option<T> is used to represent a value that might be absent.

usingRustify.Monads;// Creating Optionsvarsome=Option<string>.Some("Hello");varnone=Option<string>.None();// Pattern matchingsome.Match(some: value =>Console.WriteLine($"Got: {value}"),none:()=>Console.WriteLine("Nothing here"));// Chaining operationsvarresult=some.Map(s =>s.Length)// Option<int>.Filter(len =>len>3)// Option<int>.UnwrapOr(0);// int// Contains checkif(some.Contains("Hello")){Console.WriteLine("Found it!");}// Convert nullable framework values at the boundarystring?frameworkValue=GetFrameworkValue();varoptionValue=frameworkValue.ToOption();// Option<string>int?nullableCount=GetNullableCount();varoptionCount=nullableCount.ToOption();// Option<int>

LINQ Query Syntax

usingRustify.Monads;varoptionA=Option<int>.Some(10);varoptionB=Option<int>.Some(20);// Compose Options with LINQvarsum=fromainoptionAfrombinoptionBwherea>0&&b>0selecta+b;Console.WriteLine(sum.UnwrapOr(0));// Output: 30

Async Operations

usingRustify.Monads;varoption=Option<int>.Some(42);// Async mappingvarresult=awaitoption.MapAsync(async x =>{awaitTask.Delay(100);returnx*2;});// Async chainingvarchained=awaitoption.AndThenAsync(async x =>{vardata=awaitFetchDataAsync(x);returnOption<string>.Some(data);});// Works with Task<Option<T>> tooTask<Option<int>>optionTask=GetOptionAsync();varmapped=awaitoptionTask.MapAsync(x =>x*2);

Result<T, E>

Result<T, E> is used for functions that can return a value or an error.

usingRustify.Monads;publicenumParseError{InvalidFormat,OutOfRange}publicstaticResult<int,ParseError>ParsePositive(stringinput){if(!int.TryParse(input,outvarvalue))returnResult<int,ParseError>.Err(ParseError.InvalidFormat);if(value<=0)returnResult<int,ParseError>.Err(ParseError.OutOfRange);returnResult<int,ParseError>.Ok(value);}// Usagevarresult=ParsePositive("42");result.Match(ok: value =>Console.WriteLine($"Parsed: {value}"),err: error =>Console.WriteLine($"Error: {error}"));// Chainingvardoubled=result.Map(x =>x*2).MapErr(e =>$"Failed: {e}");// Contains checkif(result.Contains(42)){Console.WriteLine("Got 42!");}if(result.ContainsErr(ParseError.InvalidFormat)){Console.WriteLine("Invalid format error");}

LINQ Query Syntax

usingRustify.Monads;varresultA=Result<int,string>.Ok(10);varresultB=Result<int,string>.Ok(20);// Compose Results with LINQvarsum=fromainresultAfrombinresultBselecta+b;Console.WriteLine(sum.UnwrapOr(0));// Output: 30

Async Operations

usingRustify.Monads;varresult=Result<int,string>.Ok(42);// Async mappingvarmapped=awaitresult.MapAsync(async x =>{vardata=awaitProcessAsync(x);returndata;});// Async chaining with error propagationvarchained=awaitresult.AndThenAsync(async x =>{try{vardata=awaitFetchAsync(x);returnResult<string,string>.Ok(data);}catch(Exceptionex){returnResult<string,string>.Err(ex.Message);}});

Unit

Unit is used when a function doesn't return a meaningful value but needs a return type for generic contexts.

usingRustify.Monads;usingRustify.Utilities;publicstaticResult<Unit,string>SaveData(stringdata){try{File.WriteAllText("data.txt",data);returnResult<Unit,string>.Ok(Unit.New);}catch(Exceptionex){returnResult<Unit,string>.Err(ex.Message);}}// UsageSaveData("Hello").Match(ok: _ =>Console.WriteLine("Saved successfully"),err: error =>Console.WriteLine($"Failed: {error}"));

TaggedUnion<T...>

TaggedUnion provides type-safe discriminated unions for scenarios where a value can be one of several types.

2-ary TaggedUnion

usingRustify.Utilities;// Can hold either a string or an intTaggedUnion<string,int>result;// Using implicit conversionresult="success";// Becomes case 0 (string)result=42;// Becomes case 1 (int)// Using static factory methods (for type inference)varstrResult=TaggedUnion._0<string,int>("hello");varintResult=TaggedUnion._1<string,int>(100);// Pattern matching with return valuestringmessage=result.Match(case0: s =>$"Got string: {s}",case1: i =>$"Got int: {i}");// Pattern matching with actionsresult.Match(case0: s =>Console.WriteLine($"String: {s}"),case1: i =>Console.WriteLine($"Int: {i}"));// Type checkingif(result.Is0)Console.WriteLine("It's a string");if(result.Is1)Console.WriteLine("It's an int");

3-ary TaggedUnion

usingRustify.Utilities;// Useful for representing states like: Value | Warning | ErrorTaggedUnion<int,string,Exception>parseResult;parseResult=42;// Success valueparseResult="Input was negative";// Warning message parseResult=newFormatException("Bad");// Errorvaroutput=parseResult.Match(case0: value =>$"Success: {value}",case1: warning =>$"Warning: {warning}",case2: error =>$"Error: {error.Message}");

Handling Same Types

When the union contains the same type multiple times, use instance factory methods:

usingRustify.Utilities;// Both cases are int, so implicit conversion won't workvarfirst=TaggedUnion<int,int>.__0(1);// Explicitly case 0varsecond=TaggedUnion<int,int>.__1(2);// Explicitly case 1Console.WriteLine(first.Is0);// TrueConsole.WriteLine(second.Is1);// True

Arc<T> (Atomic Reference Counter)

Arc<T> allows safe sharing of data across multiple threads by using atomic operations for reference counting.

usingRustify.Utilities.Sync;publicclassSharedData{publicintValue{get;set;}}// Create shared datavararc=Arc<SharedData>.New(newSharedData{Value=42});// Clone to share ownershipvarclone1=arc.Clone();varclone2=arc.Clone();// Access the datavardata=arc.Lock();Console.WriteLine(data.Value);// Run parallel tasks with shared datavartasks=Enumerable.Range(0,5).Select(_ =>Task.Run(()=>{varlocal=arc.Clone();Console.WriteLine($"Value: {local.Lock().Value}");}));awaitTask.WhenAll(tasks);

RwLock<T> (Read-Write Lock)

RwLock<T> provides a mechanism for multiple readers or a single writer.

usingRustify.Utilities.Sync;usingRustify.Interfaces;publicclassConfig:IClone<Config>{publicstringSetting{get;set;}="";publicConfigClone()=>newConfig{Setting=Setting};}varconfigLock=newRwLock<Config>(newConfig{Setting="initial"});// Multiple concurrent readersvarreadResult=configLock.GetValue();if(readResult.IsOk()){varconfig=readResult.Unwrap();Console.WriteLine($"Setting: {config.Setting}");}// Exclusive writervarwriteResult=configLock.UpdateValue(config =>{config.Setting="updated";returnconfig;});// Async variants with cancellation supportusingvarcts=newCancellationTokenSource(TimeSpan.FromSeconds(5));varasyncRead=awaitconfigLock.GetValueAsync(cts.Token);varasyncWrite=awaitconfigLock.UpdateValueAsync(config =>{config.Setting="updated async";returnconfig;},cts.Token);

GenericMutex<T>

GenericMutex<T> provides exclusive access to a value with timeout and cancellation support.

usingRustify.Utilities.Sync;varmutex=newGenericMutex<List<int>>(newList<int>());// Update under exclusive accessvarupdateResult=mutex.UpdateValue(list =>{list.Add(42);returnlist;});// Non-blocking readvartryResult=mutex.TryGetValue();// Async with cancellationusingvarcts=newCancellationTokenSource();varasyncResult=awaitmutex.GetValueAsync(cts.Token);

API Reference

Option Methods

MethodDescription
Some(T value)Creates an Option containing a value
None()Creates an empty Option
IsSome() / IsNone()Check if Option has a value
Unwrap()Get value or throw if None
UnwrapOr(T default)Get value or return default
UnwrapOrElse(Func<T>)Get value or compute default
Map(Func<T, U>)Transform the inner value
MapOr(U default, Func<T, U>)Transform or return default
AndThen(Func<T, Option<U>>)Chain Option-returning operations
ToOption()Convert nullable reference or value types to Option
Filter(Func<T, bool>)Keep value only if predicate matches
Contains(T value)Check if Option contains a specific value
Match(some, none)Pattern match on the Option
Ok<E>() / Err<T>()Convert to Result

Result<T, E> Methods

MethodDescription
Ok(T value)Creates a successful Result
Err(E error)Creates an error Result
IsOk() / IsErr()Check Result state
Unwrap()Get value or throw if Err
UnwrapOr(T default)Get value or return default
UnwrapErr()Get error or throw if Ok
Map(Func<T, U>)Transform the success value
MapErr(Func<E, F>)Transform the error value
AndThen(Func<T, Result<U, E>>)Chain Result-returning operations
Contains(T value)Check if Result contains a specific Ok value
ContainsErr(E error)Check if Result contains a specific Err value
Match(ok, err)Pattern match on the Result
Ok() / Err()Convert to Option

Async Extension Methods

MethodDescription
MapAsync(Func<T, Task<U>>)Async transform of inner value
AndThenAsync(Func<T, Task<Option<U>>>)Async chaining for Option
AndThenAsync(Func<T, Task<Result<U, E>>>)Async chaining for Result
UnwrapOrAsync(T default)Async unwrap with default

TaggedUnion Properties & Methods

MemberDescription
Is0, Is1, Is2Check which case is active
Match(...)Pattern match on all cases
_0<...>(value), _1<...>(value)Static factory methods
__0(value), __1(value)Instance factory methods (for same-type cases)

IEquatable & IComparable Support

Both Option<T> and Result<T, E> implement IEquatable<T> and IComparable<T>:

vara=Option<int>.Some(5);varb=Option<int>.Some(5);varc=Option<int>.Some(10);Console.WriteLine(a.Equals(b));// TrueConsole.WriteLine(a.CompareTo(c));// -1 (5 < 10)// Works with sortingvaroptions=new[]{c,a,Option<int>.None()};Array.Sort(options);// None, Some(5), Some(10)

Migration Guide (v0.3.x → v0.4.0)

Breaking Changes

1. Result.IsOk/IsErr are now methods

// Before (v0.3.x)if(result.IsOk){ ...}if(result.IsErr){ ...}// After (v0.4.0)if(result.IsOk()){ ...}if(result.IsErr()){ ...}

2. Implicit conversions removed from Result

// Before (v0.3.x) - implicit conversionResult<int,string>result=42;Result<int,string>error="error message";// After (v0.4.0) - explicit factory methods requiredResult<int,string>result=Result<int,string>.Ok(42);Result<int,string>error=Result<int,string>.Err("error message");// Or using the static helper:varresult=Result.Ok<int,string>(42);varerror=Result.Err<int,string>("error message");

3. Arc.Clone() now returns a new instance

// Each clone is now a separate handle with its own lifetimeusingvararc1=Arc<Data>.New(data);usingvararc2=arc1.Clone();// arc2 is a NEW Arc instance// Both must be disposed independently// The underlying data is disposed when the last handle is released

4. ISynchronizerError replaced with SynchronizerError struct

// Before (v0.3.x)Result<T,ISynchronizerError>result=rwLock.GetValue();if(result.IsErr()&&result.UnwrapErr()==ISynchronizerError.Failed){ ...}// After (v0.4.0)Result<T,SynchronizerError>result=rwLock.GetValue();if(result.IsErr()){varerror= result.UnwrapErr();// SynchronizerError now provides richer error information:// - error.Kind (SynchronizerErrorKind enum)// - error.Message (Option<string>)// - error.InnerException (Option<Exception>)if(error.Kind==SynchronizerErrorKind.Disposed){ ...}if(error.Kind==SynchronizerErrorKind.Cancelled){ ...}if(error.Kind==SynchronizerErrorKind.Failed){ ...}}

New Features in v0.4.0

  • TaggedUnion equality: All TaggedUnion variants now implement IEquatable<T>, Equals, GetHashCode, ToString, and equality operators
  • Extended TaggedUnion: Now supports 4, 5, and 6 type parameters in addition to the original 1-3
  • Weak<T> for Arc: Break circular references with weak pointers via arc.Downgrade() and weak.Upgrade()
  • RwLockRef<T>: Callback-based read-write lock for reference types without IClone<T> requirement
  • GenericMutexError removed: Now uses SynchronizerError for consistency with other sync primitives
  • Richer error information: SynchronizerError struct provides Kind, Message, and InnerException properties
  • Improved null safety: Delegate parameters (Func/Action) now throw ArgumentNullException when null
  • IDisposable handling: GenericMutex and RwLock now properly dispose contained values that implement IDisposable

Weak<T> (Weak Reference for Arc)

Weak<T> provides weak references to Arc<T> values, allowing circular reference breaking similar to Rust's std::sync::Weak.

usingRustify.Utilities.Sync;vararc=Arc<MyClass>.New(newMyClass());// Create a weak reference (does not increment strong count)varweak=arc.Downgrade();// Check if the referenced value is still aliveif(weak.IsAlive){// Try to upgrade to a strong referencevarupgraded=weak.Upgrade();if(upgraded.IsSome()){usingvarstrongRef=upgraded.Unwrap();Console.WriteLine("Successfully upgraded weak reference");}}// After all Arc instances are disposed, Upgrade returns Nonearc.Dispose();varresult=weak.Upgrade();// Returns None

RwLockRef<T> (Callback-Based Read-Write Lock)

RwLockRef<T> provides a read-write lock for reference types that doesn't require IClone<T>. Access is through callbacks only, ensuring locks are always properly released.

usingRustify.Utilities.Sync;varrwLock=newRwLockRef<List<int>>(newList<int>{1,2,3});// Read access (multiple concurrent readers allowed)varcount=rwLock.WithRead(list =>list.Count);Console.WriteLine($"Count: {count.Unwrap()}");// Output: 3// Write access (exclusive, replaces the value)rwLock.WithWrite(list =>newList<int>{4,5,6});// Write access (exclusive, mutates in place)rwLock.WithWriteMutate(list =>list.Add(7));// Async variants with cancellation supportvarresult=awaitrwLock.WithReadAsync(async list =>{awaitTask.Delay(100);returnlist.Count;});

Contributing

Contributions are welcome! Please feel free to submit a pull request or open an issue.

License

This project is licensed under the MIT License.

About

Rustify your C# project!

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages