{"id":13629144,"url":"https://github.com/bluehands/Funicular-Switch","last_synced_at":"2025-04-17T04:33:06.951Z","repository":{"id":44751573,"uuid":"228889940","full_name":"bluehands/Funicular-Switch","owner":"bluehands","description":"Funicular-Switch is a lightweight C# port of F#'s result and option types to support 'railway oriented' programming patterns. Focus on the happy path, without loosing error information.","archived":false,"fork":false,"pushed_at":"2025-02-14T09:13:38.000Z","size":573,"stargazers_count":22,"open_issues_count":6,"forks_count":2,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-04-15T02:46:51.861Z","etag":null,"topics":["error-handling","nuget","railway-oriented-programming","result-type"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bluehands.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-12-18T17:28:40.000Z","updated_at":"2025-03-30T03:26:07.000Z","dependencies_parsed_at":"2024-01-19T09:22:30.912Z","dependency_job_id":"d513b2ef-4ea1-495a-9f7b-dc6ad0c464df","html_url":"https://github.com/bluehands/Funicular-Switch","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluehands%2FFunicular-Switch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluehands%2FFunicular-Switch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluehands%2FFunicular-Switch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluehands%2FFunicular-Switch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bluehands","download_url":"https://codeload.github.com/bluehands/Funicular-Switch/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249040165,"owners_count":21202827,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["error-handling","nuget","railway-oriented-programming","result-type"],"created_at":"2024-08-01T22:01:03.199Z","updated_at":"2025-04-17T04:33:06.943Z","avatar_url":"https://github.com/bluehands.png","language":"C#","funding_links":[],"categories":["Content"],"sub_categories":["109. [FunicularSwitch](https://ignatandrei.github.io/RSCG_Examples/v2/docs/FunicularSwitch) , in the [FunctionalProgramming](https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg-examples#functionalprogramming) category"],"readme":"# FunicularSwitch\n\n![BuildStatus](https://bluehands.visualstudio.com/bluehands%20Funicular%20Switch/_apis/build/status/bluehandsFunicularSwitch-CI?branchName=develop)\n![Try_.NET Enabled](https://img.shields.io/badge/Try_.NET-Enabled-501078.svg)\n\nFunicularSwitch is a lightweight C# port of F# result and option types.\n\nFunicularSwitch helps you to:\n\n- Focus on the 'happy path', but collect all error information.\n- Be more explicit in what our methods return.\n- Avoid deep nesting.\n- Avoid null checks and eventual properties (properties only relevant for a certain state of an object), use Result or Option instead.\n- Comfortably write async code pipelines.\n- Wrap third party library exceptions / return values into results at the code level were we really understand what is happening.\n\n# Getting Started\n\n### Packages\n\n - [NuGet: FunicularSwitch](https://www.nuget.org/packages/FunicularSwitch/)\n - [NuGet: FunicularSwitch.Generators](https://www.nuget.org/packages/FunicularSwitch.Generators/)\n\n[**FunicularSwitch**](https://github.com/bluehands/Funicular-Switch#funicularswitch-usage) is a library containing the Result and Option type. Usage and the general idea is described in the following sections. The 'Error' type is always string, which allows natural concatenation and is sufficient in many cases.\n\n[**FunicularSwitch.Generators**](https://github.com/bluehands/Funicular-Switch#funicularswitchgenerators-usage) is a C# source generator package (projects consuming it, will have no runtime dependency to any FunicularSwitch dll). With this source generator you can have a result type with the very same behaviour as FunicularSwitch.Result but a custom error type (instead of string) by just annotating a class with the [`ResultType`](https://github.com/bluehands/Funicular-Switch#resulttype-attribute) attribute. That means you are free to represent failures in a way suitable for your needs. \nA second thing coming with this package is the [`UnionType`](https://github.com/bluehands/Funicular-Switch#-uniontype-attribute) attribute. This offers a way to have F# like unions with generated Match methods, forcing consumers to handle all cases (exhaustive matching). They allow for compiler safe switches handling all concrete subtypes of a base class. As a third thing the same Match methods are also generated for enum types annotated with the [`ExtendedEnum`](https://github.com/bluehands/Funicular-Switch#extendedenum-attribute) attribute.\n\n# \u003ca name=\"funicular_usage\"\u003e\u003c/a\u003eFunicularSwitch Usage\n\n*This document is created using [dotnet try](https://github.com/dotnet/try/blob/main/DotNetTryLocal.md). If you have dotnet try global tool installed, just clone the repo, type `dotnet try` on top level and play around with all code samples in your browser while reading.*\n\nThis following section mainly focuses on `Result`. `Result` is a union type representing either Ok or the Error case just like F#s Result type. For FunicularSwitch the error type is `String` for sake of simplicity (Using types with multiple generic arguments is quite verbose in C#).\n\nResult should be used in all places, were something can go wrong. Doing so it replaces exceptions and null/default return values.\n\nCreating a `Result` is easy:\n\n``` cs --region resultCreation --source-file Source/DocSamples/ReadmeSamples.cs --project Source/DocSamples/DocSamples.csproj\n//Ok result:\nvar fortyTwo = Result.Ok(42);\n//or using implicit cast operator\nResult\u003cstring\u003e ok = \"Ok\";\n\n//Error result:\nvar error = Result.Error\u003cint\u003e(\"Could not find the answer\");\n```\n\nNow lets follow the happy path, do something, if everything was ok. `Map`:\n\n``` cs --region map --source-file Source/DocSamples/ReadmeSamples.cs --project Source/DocSamples/DocSamples.csproj --session map\nstatic Result\u003cint\u003e Ask() =\u003e 42;\n\nResult\u003cint\u003e answerTransformed = Ask()\n    .Map(answer =\u003e answer * 2);\n\nConsole.WriteLine(answerTransformed);\n```\n\n``` console --session map\nOk 84\n\n```\n\nor do something that might fail, if everything was ok. `Bind`:\n\n``` cs --region bind --source-file Source/DocSamples/ReadmeSamples.cs --project Source/DocSamples/DocSamples.csproj --session bind\nstatic Result\u003cint\u003e Ask() =\u003e 42;\n\nResult\u003cint\u003e answerTransformed = Ask()\n    .Bind(answer =\u003e answer == 0 ? Result.Error\u003cint\u003e(\"Division by zero\") : 42 / answer);\n\nConsole.WriteLine(answerTransformed);\n```\n\n``` console --session bind\nOk 1\n\n```\n\nThe lambdas passed to `Map` and `Bind` are only invoked if everything went well so far, otherwise you are on the error track were error information is passed on 'invisibly':\nb\n\n``` cs --region errorPropagation --source-file Source/DocSamples/ReadmeSamples.cs --project Source/DocSamples/DocSamples.csproj --session errorPropagation\nstatic Result\u003cint\u003e Transform(Result\u003cint\u003e result) =\u003e\n                result\n                    .Bind(answer =\u003e answer == 0 ? Result.Error\u003cint\u003e(\"Division by zero\") : 42 / answer)\n                    .Map(transformed =\u003e transformed * 2);\n\nResult\u003cint\u003e firstLevelError = Transform(Result.Error\u003cint\u003e(\"I don't know\"));\nConsole.WriteLine($\"First level: {firstLevelError}\");\n\nResult\u003cint\u003e secondLevelError = Transform(Result.Ok(0));\nConsole.WriteLine($\"Second level: {secondLevelError}\");\n```\n\n``` console --session errorPropagation\nFirst level: Error I don't know\nSecond level: Error Division by zero\n\n```\n\nFinally you might want to leave the `Result` world, so you have to take care of the error case as well (that's a good thing!). `Match`:\n\n``` cs --region match --source-file Source/DocSamples/ReadmeSamples.cs --project Source/DocSamples/DocSamples.csproj --session match\nstatic Result\u003cint\u003e Ask() =\u003e 42;\n\nstring whatIsIt =\n    Ask().Match(\n        answer =\u003e $\"The answer is: {answer}\",\n        error =\u003e $\"Ups: {error}\"\n    );\n\nConsole.WriteLine(whatIsIt);\n```\n\n``` console --session match\nThe answer is: 42\n\n```\n\nThose are basically the four (actually three) main operations on `Result` - `Create`, `Bind`, `Map` and `Match`. There are a lot of overloads and other helpers in FunicularSwitch to avoid repetition of `Result` specific patterns like:\n\n- 'Combine results to Ok if everything is Ok otherwise collect errors' - `Aggregate`, `Map` and `Bind` overloads on collections\n- 'Ok if at least one item passes certain validations, otherwise collect info why no one matched' - `FirstOk`\n- 'Ok if item from a dictionary was found, otherwise (nice) error' - `TryGetValue` extension on Dictionary\n- 'Ok if type T is `as` convertible to T1, error otherwise' - 'As' extension returning Result\n- 'Ok if item is valid regarding custom validations, error otherwise' - `Validate`\n- 'Async support' - `Map` `Bind` and `Aggregate` overloads with async lambdas and extensions defined on Task\u003c...\u003e\n- ...\n\nIf you miss functionality it can be added easily by writing your own extension methods. If it is useful for us all don't hesitate to make pull request. Finally a little example demonstrating some of the functionality mentioned above (validation, aggregation, async pipeline). Lets cook:\n\n``` cs --region fruitSalad --source-file Source/DocSamples/ReadmeSamples.cs --project Source/DocSamples/DocSamples.csproj --session fruitSalad\npublic static async Task FruitSalad()\n{\n    var stock = ImmutableList.Create(\n        new Fruit(\"Orange\", 155),\n        new Fruit(\"Orange\", 12),\n        new Fruit(\"Apple\", 132),\n        new Fruit(\"Stink fruit\", 1));\n\n    var ingredients = ImmutableList.Create(\"Apple\", \"Banana\", \"Pear\", \"Stink fruit\");\n\n    const int cookSkillLevel = 3;\n\n    static IEnumerable\u003cstring\u003e CheckFruit(Fruit fruit)\n    {\n        if (fruit.AgeInDays \u003e 20)\n            yield return $\"{fruit.Name} is not fresh\";\n\n        if (fruit.Name == \"Stink fruit\")\n            yield return \"Stink fruit, I do not serve that\";\n    }\n\n    var salad =\n        await ingredients\n            .Select(ingredient =\u003e\n                stock\n                    .Where(fruit =\u003e fruit.Name == ingredient)\n                    .FirstOk(CheckFruit, onEmpty: () =\u003e $\"No {ingredient} in stock\")\n                )\n            .Bind(fruits =\u003e CutIntoPieces(fruits, cookSkillLevel))\n            .Map(Serve);\n\n    Console.WriteLine(salad.Match(ok =\u003e \"Salad served successfully!\", error =\u003e $\"No salad today:{Environment.NewLine}{error}\"));\n}\n\nstatic Result\u003cSalad\u003e CutIntoPieces(IEnumerable\u003cFruit\u003e fruits, int skillLevel = 5)\n{\n    try\n    {\n        return CutFruits(fruits, skillLevel);\n    }\n    catch (Exception e)\n    {\n        return Result.Error\u003cSalad\u003e($\"Ouch: {e.Message}\");\n    }\n}\n\nstatic Salad CutFruits(IEnumerable\u003cFruit\u003e fruits, int skillLevel) =\u003e skillLevel \u003e 5 ? new Salad(fruits) : throw new Exception(\"Cut my fingers\");\nstatic Task\u003cSalad\u003e Serve(Salad salad) =\u003e Task.FromResult(new Salad(salad.Fruits, true));\n\nclass Salad\n{\n    public IReadOnlyCollection\u003cFruit\u003e Fruits { get; }\n    public bool Served { get; }\n\n    public Salad(IEnumerable\u003cFruit\u003e fruits, bool served = false)\n    {\n        Fruits = fruits.ToList();\n        Served = served;\n    }\n}\n\nclass Fruit\n{\n    public string Name { get; }\n    public int AgeInDays { get; }\n\n    public Fruit(string name, int ageInDays)\n    {\n        Name = name;\n        AgeInDays = ageInDays;\n    }\n}\n```\n\n``` console --session fruitSalad\nNo salad today:\nApple is not fresh\nNo Banana in stock\nNo Pear in stock\nStink fruit, I do not serve that\n```\nAs you can see, all errors are collected as far as possible. Feel free to play around with the cooks skill level, fruits in stock and the ingredients list to finally get your fruit salad.\n\n# \u003ca name=\"generators_usage\"\u003e\u003c/a\u003eFunicularSwitch.Generators Usage\n\n## ResultType attribute\n\nAfter adding the FunicularSwitch.Generators package you can mark a class as result type using the `ResultType` attribute. The class has to be abstract and partial with a single generic argument. Ok and Error cases, Map, Bind, Match and some other methods will be generated so you can use your Result just like the one from the FunicularSwitch package. We recommend using a [UnionType](https://github.com/bluehands/Funicular-Switch#uniontype) as error type but you are free to use any type you want to represent failures.\n\n``` cs\n  [FunicularSwitch.Generators.ResultType(ErrorType = typeof(MyCustomError))]\n  public abstract partial class Result\u003cT\u003e {}\n```\n\n### Exceptions\n\nTo turn all exceptions that might happen during your map, bind, validate, etc. calls into error results, write a static conversion method and mark it with the `ExceptionToError` attribute:\n``` cs\npublic static class MyCustomErrorExtension\n{\n  [FunicularSwitch.Generators.ExceptionToError]\n  public static MyCustomError ToGenericError(Exception ex) =\u003e ...\n}\n```\nHaving the ExceptionToError method, a call like `Ok(42).Map(i =\u003e 42 / 0)` will return an error result with an error produced by your custom method instead of throwing a DivisionByZero exception.  \n\n##### Considerations:\nUsing the `ExceptionToError` attribute is actually a decision that points into a direction that is different from the way Result is implemented in F#, were Result and the correspondind Error type are meant to model expected domain errors (see [fsharpforfunandprofit blog post](https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/)). You will still have to handle exceptions on the highest parts of your system and there is no 'fail fast' because early exceptions always travel through your hole Result chain.\n\n### Combine results\n\nIf your errors can be combined, write an attributed extension method or a member method on your error type that combines two errors into one\n``` cs\npublic static class MyCustomErrorExtension\n{\n  [FunicularSwitch.Generators.MergeError]\n  public static MyCustomError Merge(this MyCustomError error, MyCustomError other) =\u003e ...\n}\n```\nand a bunch of methods like `Aggregate`, `Validate`, `AllOk`, `FirstOk` and more will appear that make use of the fact that errors can be merged.\n\n## \u003ca name=\"uniontype\"\u003e UnionType attribute\n\nThere is another useful generator coming with the package. Adding the `UnionType` attribute to a base record / class or interface makes `Match` extension methods appear for this type. They are also inspired by F# where a match expression has to cover all cases and the compiler helps you with that. Assuming you implemented an error type as a base type and one derived type for every kind of error:\n\n``` cs\n[FunicularSwitch.Generators.UnionType]\npublic abstract class Error{...}\n\npublic sealed class NotFound : Error {...}\npublic sealed class Failure : Error {...}\npublic sealed class InvalidInput : Error {...}\n```\n\nthe generator detecting the `[UnionType]` adds Match methods so you can write:\n\n``` cs\nstatic string PrintError(Error error) =\u003e\n        error.Match(\n                notFound =\u003e $\"Not found: {notFound.Message}\",\n                failure =\u003e $\"Ups, something went wrong: {failure.Message} - {failure.Exception}\",\n                invalidInput =\u003e $\"Name was invalid: {invalidInput.Message}\"                \n            );\n```\n\nIf you decide to add a case to your Error union all consuming switches break and you never miss a case at runtime!\n\nMatch methods are also provided for async case handlers and as extensions on `Task\u003cError\u003e`.\n\nThere are also `Switch` extension methods generated which are the 'void' versions of `Match`, although this is not recommended from a functional point of view :).\n\n``` cs\nstatic void PrintIfNotFound(Error error) =\u003e\n        error.Switch(\n                notFound =\u003e Console.WriteLine($\"Not found: {notFound.Message}\"),\n                failure =\u003e { /*ignore*/ },\n                invalidInput =\u003e { /*ignore*/ }\n            );\n```\n\nTo avoid bad surprises a well defined order of parameters of Match methods is crucial. By default parameters are generated in alphabetical order. This behaviour can be adapted using the `CaseOrder` argument on `UnionType` attribute (FunicularSwitch.Generators namespace omitted):\n\n``` cs\n//default\n[UnionType(CaseOrder = CaseOrder.Alphabetical)]\npublic abstract class Error{...}\n\n//useful for union types the define their cases as nested subclasses in a well defined order\n[UnionType(CaseOrder = CaseOrder.AsDeclared)]\npublic abstract class Error{...}\n\n//order defined explicitly. Case sort index with [UnionCase] attribute on derived types is expected (generator warning if missing or ambigous)\n[UnionType(CaseOrder = CaseOrder.Explicit)]\npublic abstract class Error{...}\n\n[UnionCase(index: 0)]\npublic sealed class NotFound : Error {...}\n[UnionCase(index: 20)]\npublic sealed class Failure : Error {...}\n[UnionCase(index: 10)]\npublic sealed class InvalidInput : Error {...}\n```\n\n#### Static factory methods\nIf your base type is a partial record or class, static factory methods for your derived cases are added: \n\n``` cs\n[UnionType]\npublic abstract partial record Error;\n\npublic record NotFound(int Id, string? Message = \"Not found\") : Error;\npublic record InvalidInput(string Message) : Error;\n\nclass ExampleConsumer\n{\n    public static void UseGeneratedFactoryMethods()\n    {\n        var notFound = Error.NotFound(42); //default value is pulled up to factory methods.\n        var invalid = Error.InvalidInput(\"I don't like it\");\n    }\n}\n```\n\nThose factory methods are not generated if they would conflict with an existing field, property or method on the base type. \nSo you can always decide to implement them by yourself. Generation of factory methods on a partial base type can be suppressed \nby setting StaticFactoryMethods argument to false: `[UnionType(StaticFactoryMethods=false)]`. Currently default values in \nconstructor parameters from namespaces other than System need full qualification.\nIf you like to declare your cases as nested types of your base types you can use an underscore prefix or postfix with your nested type name to avoid conflicts with factory methods. Static factory method will then be generated without the underscore. This also works if you use the base type name as prefix or postfix.\n\n``` cs\n[UnionType]\npublic abstract partial record Failure\n{\n    public record NotFound_(int Id) : Failure;\n}\n\npublic record InvalidInputFailure(string Message) : Failure;\n\nclass ExampleConsumer\n{\n    public static void UseGeneratedFactoryMethods()\n    {\n        var notFound = Failure.NotFound(42); //static factory method generated without underscore used in typename NotFound_ \n        var invalid = Failure.InvalidInput(\"I don't like it\"); //static factory method generated without base typename postfix \n    }\n}\n```\n\nBase types of unions may also be generic types with arbitrary number of type parameters. Case types with generic arguments are not supported.\n\n## ExtendedEnum attribute\n\nThe `ExtendedEnum` attribute works like `UnionType` but for enums:\n\n``` cs\n[FunicularSwitch.Generators.ExtendedEnum]\npublic enum PlatformIdentifier\n{\n    LinuxDevice,\n    DeveloperMachine,\n    WindowsDevice\n}\n```\n\nthe generator detecting the `[ExtendedEnum]` adds Match methods so you can write:\n\n``` cs\nvar isGraphicalLinux = PlatformIdentifier.LinuxDevice\n    .Match(\n        developerMachine: () =\u003e false,\n        linuxDevice: () =\u003e true,\n        windowsDevice: () =\u003e true\n    );\n```\n\nThe default case order for `ExtendedEnum` is AsDeclared. To avoid problems with changing case orders, one should always use named parameters in Match and Switch calls!\n\nTo generate Match extensions for all types in an assembly use the `ExtendEnums` attribute. Flags enums an enums with duplicate values are omitted:\n\n``` cs\n//generate internal Match extension methods for all enums in System (Containing assembly of System.DateTime). \n[assembly: ExtendEnums(typeof(System.DateTime), Accessibility = ExtensionAccessibility.Internal)]\n\n//shortcut to generate Match extension methods for all enums in current assembly\n[assembly: ExtendEnums]\n```\n\nTo generate Match extensions for a specific type in an assembly write:\n\n```\n[assembly: ExtendEnum(typeof(DateTimeKind), CaseOrder = EnumCaseOrder.Alphabetic)]\n```\n\n### Additional documentation\n\n[Tutorial markdown](https://github.com/bluehands/Funicular-Switch/blob/main/TUTORIAL.md)\n\n[Tutorial source](https://github.com/bluehands/Funicular-Switch/tree/main/Source/Tutorial)\n\n# Contributing\n\nWe're looking forward to pull requests.\n\n# Versioning\n\nWe use [SemVer](http://semver.org/) for versioning.\n\n# Authors\n\nbluehands.de\n\n# License\n\n[MIT License](https://github.com/bluehands/Funicular-Switch/blob/main/LICENSE)\n\n# Acknowledgments\n\n[F# for fun and profit: Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/)\n\n[F# for fun and profit: Map and Bind and Apply, Oh my!](https://fsharpforfunandprofit.com/series/map-and-bind-and-apply-oh-my.html)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbluehands%2FFunicular-Switch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbluehands%2FFunicular-Switch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbluehands%2FFunicular-Switch/lists"}