Last active
December 2, 2022 19:51
-
-
Save arturaz/3f18d80ee9172f7699c6b7341ca2da96 to your computer and use it in GitHub Desktop.
Practical demonstration of config diffing with higher kinded types in C#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using JetBrains.Annotations; | |
namespace FPCSharpUnity.core.functional.higher_kinds; | |
/// <summary> | |
/// As <see cref="Func{A,B}"/>, but transforms the functor W1[_] to W2[_], instead of transforming the value. | |
/// </summary> | |
[PublicAPI] public interface FuncK<W1, W2> { | |
HigherKind<W2, A> apply<A>(HigherKind<W1, A> value); | |
} | |
/// <summary> | |
/// As <see cref="Func{A,B,C}"/>, but transforms the functor W1[_] and W2[_] to WR[_], instead of transforming the | |
/// values. | |
/// </summary> | |
[PublicAPI] public interface FuncK<W1, W2, WR> { | |
HigherKind<WR, A> apply<A>(HigherKind<W1, A> value1, HigherKind<W2, A> value2); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using JetBrains.Annotations; | |
using FPCSharpUnity.core.concurrent; | |
using FPCSharpUnity.core.exts; | |
namespace FPCSharpUnity.core.functional.higher_kinds; | |
[PublicAPI] public interface Functor<Witness> { | |
HigherKind<Witness, B> map<A, B>(HigherKind<Witness, A> data, Func<A, B> mapper); | |
} | |
[PublicAPI] public class Functors : | |
Functor<Id.W>, Functor<Option.W> | |
{ | |
public static readonly Functors i = new Functors(); | |
protected Functors() {} | |
public HigherKind<Id.W, B> map<A, B>(HigherKind<Id.W, A> data, Func<A, B> mapper) => | |
Id.a(mapper(data.narrowK().a)); | |
public HigherKind<Option.W, B> map<A, B>(HigherKind<Option.W, A> data, Func<A, B> mapper) => | |
data.narrowK().map(mapper); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using GenerationAttributes; | |
using JetBrains.Annotations; | |
namespace FPCSharpUnity.core.functional.higher_kinds; | |
[PublicAPI] public static partial class Id { | |
public struct W {} | |
public static Id<A> narrowK<A>(this HigherKind<W, A> hkt) => (Id<A>) hkt; | |
} | |
/// <summary>Id monad is a way to lift a value into a monad when dealing with higher-kinded code.</summary> | |
[PublicAPI, Record(ConstructorFlags.Apply)] | |
public readonly partial struct Id<A> : HigherKind<Id.W, A> { | |
public readonly A a; | |
public static implicit operator A(Id<A> id) => id.a; | |
public static implicit operator Id<A>(A a) => new Id<A>(a); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using FPCSharpUnity.core.config; | |
using FPCSharpUnity.core.exts; | |
using FPCSharpUnity.core.functional; | |
using FPCSharpUnity.core.functional.higher_kinds; | |
using FPCSharpUnity.core.json; | |
using FPCSharpUnity.core.test_framework; | |
using FPCSharpUnity.core.utils; | |
using GenerationAttributes; | |
using NUnit.Framework; | |
namespace HKTConfigDiff; | |
/// <summary> | |
/// Some configuration for the game, parametrized with a higher-kinded type, | |
/// such as <see cref="Id{A}"/> or <see cref="Option{A}"/>. | |
/// </summary> | |
[Record(ConstructorFlags.Apply | ConstructorFlags.Withers)] | |
public partial class GameConfig<W> { | |
public readonly HigherKind<W, int> hitPoints; | |
public readonly HigherKind<W, string> name; | |
} | |
public static class TestDiffing { | |
[Test] | |
public static void test() { | |
var config = GameConfig.a(hitPoints: Id.a(100), name: Id.a("Archer")); | |
var newConfig = config.withHitPoints(Id.a(200)); | |
newConfig.shouldEqual(GameConfig.a(hitPoints: Id.a(200), name: Id.a("Archer"))); | |
var configDiff = config.zip(newConfig, new GameConfig.DiffFuncK()); | |
configDiff.shouldEqual(GameConfig.a( | |
hitPoints: Some.a(200), name: Option<string>.None | |
)); | |
var diffAppliedConfig = config.zip(configDiff, new GameConfig.DiffApplyFuncK()); | |
diffAppliedConfig.shouldEqual(GameConfig.a( | |
hitPoints: Id.a(200), name: Id.a("Archer") | |
)); | |
diffAppliedConfig.shouldEqual(newConfig); | |
} | |
} | |
public static partial class GameConfigExts { | |
/// <summary> | |
/// Given two <see cref="GameConfig{W}"/> instances and a mapper produces a result | |
/// which joins the two instances somehow. | |
/// </summary> | |
public static GameConfig<W2> zip<W, W1, W2>( | |
this GameConfig<W> cfg, GameConfig<W1> cfg1, FuncK<W, W1, W2> mapper | |
) => GameConfig.a( | |
hitPoints: mapper.apply(cfg.hitPoints, cfg1.hitPoints), | |
name: mapper.apply(cfg.name, cfg1.name) | |
); | |
} | |
public static partial class GameConfig { | |
/// <summary> | |
/// Takes an older and newer version of a value and returns `Some` if they differ, | |
/// `None` if they are equal. | |
/// </summary> | |
public class DiffFuncK : FuncK<Id.W, Id.W, Option.W> { | |
public HigherKind<Option.W, A> apply<A>( | |
HigherKind<Id.W, A> value1, HigherKind<Id.W, A> value2 | |
) { | |
var eq = System.Collections.Generic.EqualityComparer<A>.Default; | |
A val1 = value1.narrowK().a; | |
A val2 = value2.narrowK().a; | |
return eq.Equals(val1, val2) ? Option<A>.None : Some.a(val2); | |
} | |
} | |
/// <summary> | |
/// Takes a value and maybe an updated value and returns the updated value if it's | |
/// present or old value otherwise. | |
/// </summary> | |
public class DiffApplyFuncK : FuncK<Id.W, Option.W, Id.W> { | |
public HigherKind<Id.W, A> apply<A>( | |
HigherKind<Id.W, A> value1, HigherKind<Option.W, A> value2 | |
) => | |
// ReSharper disable once ConvertClosureToMethodGroup - doesn't compile | |
// otherwise. | |
value2.narrowK().fold(ifNone: value1, ifSome: a => Id.a(a)); | |
} | |
} | |
#region JSON example | |
public static class TestJSON { | |
[Test] | |
public static void jsonSerialization() { | |
// Parse the full configuration from JSON. | |
GameConfig<Id.W> config = Config.parseJsonObject( | |
@"{""hitPoints"":100,""name"":""Archer""}", GameConfig.parser | |
).rightOrThrow; | |
// Serialize the full configuration to JSON. | |
var fullConfigJson = config.toJson( | |
Functors.i, | |
valueToJson: (name, jsonValueIdHkt) => { | |
Id<JsonValue> jsonValueId = jsonValueIdHkt.narrowK(); | |
JsonValue jsonValue = jsonValueId.a; | |
return JsonObject.a(KV.a(name, jsonValue)); | |
} | |
); | |
fullConfigJson.asVal.serialize().shouldEqual( | |
@"{""hitPoints"":100,""name"":""Archer""}" | |
); | |
// Parse the config difference from JSON. | |
var configDiff = Config.parseJsonObject( | |
@"{""hitPoints"":200}", GameConfig.diffParser | |
).rightOrThrow; | |
configDiff.shouldEqual(GameConfig.a( | |
hitPoints: Some.a(200), name: Option<string>.None | |
)); | |
// Serialize the config difference to JSON. | |
var configDiffJson = configDiff.toJson( | |
Functors.i, | |
valueToJson: (name, value) => { | |
Option<JsonValue> maybeJsonValue = value.narrowK(); | |
return maybeJsonValue.fold( | |
ifNone: JsonObject.empty, | |
ifSome: jsonValue => JsonObject.a(KV.a(name, jsonValue)) | |
); | |
} | |
); | |
configDiffJson.asVal.serialize().shouldEqual(@"{""hitPoints"":200}"); | |
} | |
} | |
public static partial class GameConfigExts { | |
public static JsonObject toJson<W>( | |
this GameConfig<W> cfg, | |
Functor<W> functor, | |
Func<string, HigherKind<W, JsonValue>, JsonObject> valueToJson | |
) => | |
valueToJson("hitPoints", functor.map(cfg.hitPoints, hp => JsonValue.num(hp))) | |
+ valueToJson("name", functor.map(cfg.name, JsonValue.str)); | |
} | |
public static partial class GameConfig { | |
public static readonly Config.Parser<JsonValue, GameConfig<Id.W>> parser = | |
Config.configParser.flatMapTry((_, cfg) => a( | |
hitPoints: Id.a(cfg.getInt("hitPoints")), | |
name: Id.a(cfg.getString("name")) | |
)); | |
public static readonly Config.Parser<JsonValue, GameConfig<Option.W>> diffParser = | |
Config.configParser.flatMapTry((_, cfg) => a( | |
hitPoints: cfg.optInt("hitPoints"), | |
name: cfg.optString("name") | |
)); | |
} | |
#endregion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment