From 1331326225831feb35b01dd209197ca607d74a91 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 4 Jun 2024 21:14:08 +0200 Subject: [PATCH] Updates --- 404.html | 2 +- assets/js/5e06daff.6d3b9cb0.js | 1 + assets/js/5e06daff.85b2920d.js | 1 - assets/js/831440af.66ef3cd6.js | 1 - assets/js/831440af.c04aa112.js | 1 + assets/js/984a9349.63231122.js | 1 - assets/js/984a9349.ac019b8e.js | 1 + assets/js/fa4d91bf.4e7afb4d.js | 1 + assets/js/fa4d91bf.ab1de002.js | 1 - .../js/{runtime~main.60460aff.js => runtime~main.5550df67.js} | 2 +- blog/Library Design/index.html | 3 ++- blog/archive/index.html | 2 +- blog/atom.xml | 1 + blog/index.html | 3 ++- blog/rss.xml | 1 + blog/tags/api/index.html | 3 ++- blog/tags/design/index.html | 3 ++- blog/tags/fable/index.html | 3 ++- blog/tags/helper/index.html | 3 ++- blog/tags/index.html | 2 +- blog/tags/library/index.html | 3 ++- docs/Graphs/Block/index.html | 2 +- docs/Graphs/ClassDiagram/index.html | 2 +- docs/Graphs/ErDiagram/index.html | 2 +- docs/Graphs/FlowChart/index.html | 2 +- docs/Graphs/Gantt/index.html | 2 +- docs/Graphs/Git/index.html | 2 +- docs/Graphs/Journey/index.html | 2 +- docs/Graphs/Mindmap/index.html | 2 +- docs/Graphs/PieChart/index.html | 2 +- docs/Graphs/Quadrant/index.html | 2 +- docs/Graphs/Requirement/index.html | 2 +- docs/Graphs/Sankey/index.html | 2 +- docs/Graphs/Sequence/index.html | 2 +- docs/Graphs/StateDiagram/index.html | 2 +- docs/Graphs/Timeline/index.html | 2 +- docs/Graphs/XYChart/index.html | 2 +- docs/TabTemplates/index.html | 2 +- docs/config/index.html | 2 +- docs/installation/index.html | 4 ++-- docs/theme/index.html | 2 +- docs/themeVariables/index.html | 2 +- index.html | 2 +- markdown-page/index.html | 2 +- my-markdown-page/index.html | 2 +- my-react-page/index.html | 2 +- 46 files changed, 50 insertions(+), 41 deletions(-) create mode 100644 assets/js/5e06daff.6d3b9cb0.js delete mode 100644 assets/js/5e06daff.85b2920d.js delete mode 100644 assets/js/831440af.66ef3cd6.js create mode 100644 assets/js/831440af.c04aa112.js delete mode 100644 assets/js/984a9349.63231122.js create mode 100644 assets/js/984a9349.ac019b8e.js create mode 100644 assets/js/fa4d91bf.4e7afb4d.js delete mode 100644 assets/js/fa4d91bf.ab1de002.js rename assets/js/{runtime~main.60460aff.js => runtime~main.5550df67.js} (69%) diff --git a/404.html b/404.html index b8433af..f755acc 100644 --- a/404.html +++ b/404.html @@ -5,7 +5,7 @@ Page Not Found | Siren - + diff --git a/assets/js/5e06daff.6d3b9cb0.js b/assets/js/5e06daff.6d3b9cb0.js new file mode 100644 index 0000000..df66e03 --- /dev/null +++ b/assets/js/5e06daff.6d3b9cb0.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2338],{6426:e=>{e.exports=JSON.parse('{"blogPosts":[{"id":"Library Design","metadata":{"permalink":"/Siren/blog/Library Design","editUrl":"https://github.com/Freymaurer/Siren/tree/main/docs/blog/2024-05-06-fable-library-design.mdx","source":"@site/blog/2024-05-06-fable-library-design.mdx","title":"Library Design","description":"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#.","date":"2024-05-06T00:00:00.000Z","tags":[{"label":"fable","permalink":"/Siren/blog/tags/fable"},{"label":"library","permalink":"/Siren/blog/tags/library"},{"label":"api","permalink":"/Siren/blog/tags/api"},{"label":"design","permalink":"/Siren/blog/tags/design"},{"label":"helper","permalink":"/Siren/blog/tags/helper"}],"readingTime":23.63,"hasTruncateMarker":false,"authors":[{"name":"Kevin Frey","title":"Maintainer of Siren","url":"https://github.com/Freymaurer","imageURL":"https://github.com/Freymaurer.png","key":"freyk"}],"frontMatter":{"slug":"Library Design","title":"Library Design","authors":"freyk","tags":["fable","library","api","design","helper"]},"unlisted":false},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n\\nSiren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#.\\n\\n:::tip[Fable.Multiverse]\\n\\nTo make it as easy as possible to create a Fable library that publishes in multiple languages, I created a template called [Fable.Multiverse](https://github.com/Freymaurer/Fable.Multiverse) :tada:\\n\\n:::\\n\\n## Idea \ud83d\udca1\\n\\nDuring a hackathon for our research data management consortium, we were discussing ideas for visualizing graph like structures in a way that allows easy gitlab integration and can be understood and used by any level of user. The idea we pursued was to add `.md` files with mermaid graphs for an easy overview. So why not write a domain specific language for mermaid graphs to make creation of such graphs easier and more error proof. Good idea, but what programming language should we use? In our consortium we have several groups, some using JavaScript, some Python, some (us) .NET or more specifically F#. Because i already have quite some experience using [Fable][Fable], i did the mental checklist to see if it would be a good fit for this project.\\n\\n- [x] *Does not need any dependencies.* Mermaid graphs, are mostly YAML, so no complex syntax.\\n- [x] *Does not require IO interaction.* We can simply focus on writing our mermaid graph as string and allow the user to do whatever they want with it.\\n- [x] *Only Fable compatible languages needed.* We are already very happy offering such a tool in Python, JavaScript and F#.\\n\\n.. and thats it \ud83c\udf89 So we can start developing a libary with one codebase for 4 [5] languages.\\n\\n## What is Fable?\\n\\nFable is a F# to X transpiler. It started out targeting only JavaScript, using a naming reference to the popular [Babel](https://babeljs.io) JavaScript transpiler. Now Fable aims to support multiple languages, all in different states. At the time of writing, the offical Fable docs state the following:\\n\\n| Language | Status | \\n|---|---|\\n| JavaScript | Stable |\\n| TypeScript |\\tStable| \\n| Dart |\\tBeta| \\n| Python |\\tBeta| \\n| Rust |\\tAlpha| \\n| PHP |\\tExperimental| \\n\\n### Benefits\\n\\n- **Type Safety**. F# is a statically typed language, which means that the compiler can catch many errors before they even happen.\\n- **Lightweight Syntax**. F# has a very lightweight syntax, which makes it easy to read and write. It does not require a lot of boilerplate code and you can get right into the meat of your program.\\n- **Testing(?)**. Main codebase is written in F# as well as most tests. This allows us to also transpile the tests to other languages and run them there. This is a big advantage, as we can be sure that the tests are the same in all languages.\\n\\n:::note[But why the question mark behind testing?]\\n\\nBecause we can recycle the tests, to ensure correct functionality, but we still must test if the library can be used from all supported languages without hurdles.\\n:::\\n\\n## API Design\\n\\nTo make the code look and feel as native as possible in all languages, there are some things we need to consider. But first let us have a look at fable transpiled code.\\n\\n:::info\\nThe following code will use the [Fable REPL][REPL] to transpile code for easy showcasing!\\n:::\\n\\n\\n\\n\\n```fsharp\\nlet helloWorld = printfn \\"Hello World\\"\\n```\\n\\n\\n\\n```js\\nimport { printf, toConsole } from \\"fable-library-js/String.js\\";\\n\\nexport const helloWorld = toConsole(printf(\\"Hello World\\"));\\n```\\n\\n\\n\\n```python\\nfrom fable_library_js.string import (to_console, printf)\\n\\nhello_world: None = to_console(printf(\\"Hello World\\"))\\n```\\n\\n\\n\\nWe can already notice some things:\\n\\n- Fable tries to transpile into native syntax, so for example snake_case in Python and camelCase in JavaScript\\n- Fable has some wrappers for functions which might have native equivalents. In F# `printfn` is used to print to the console, in JavaScript `console.log` and in Python `print`. But Fable uses their own printf function to ensure 100% correct f# transpilation.\\n\\nNext we will have a look at a class with some member functions.\\n\\n\\n\\n```fsharp\\ntype MyClass =\\n static member add (x: int) (y: int) = x + y\\n```\\n\\n\\n\\n```js\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n}\\n\\nexport function MyClass_$reflection() {\\n return class_type(\\"Test.MyClass\\", undefined, MyClass);\\n}\\n\\nexport function MyClass_add(x, y) {\\n return x + y;\\n}\\n```\\n\\n\\n\\n```python\\nfrom fable_library_js.reflection import (TypeInfo, class_type)\\n\\ndef _expr0() -> TypeInfo:\\n return class_type(\\"Test.MyClass\\", None, MyClass)\\n\\n\\nclass MyClass:\\n ...\\n\\nMyClass_reflection = _expr0\\n\\ndef MyClass_add(x: int, y: int) -> int:\\n return x + y\\n```\\n\\n\\n\\nOh no, this does not look good. Fable does a thing called [*name mangling*](https://fable.io/docs/javascript/features.html#name-mangling). \\nHave a look at the offical docs for a deeper view on this topic.\\nFor now its enought to know, this is done to allow overloading functions in F#.\\n\\n:::info[Edit- Tree-Shaking]\\n[ncave][ncave] pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.\\n:::\\n\\n### Using `[]`\\n\\nBut we can tell Fable that we know what we are doing and ignore name mangling.\\n\\n\\n\\n```fsharp\\nopen Fable.Core\\n\\n[]\\ntype MyClass =\\n static member add (x: int) (y: int) = x + y\\n```\\n\\n\\n\\n```js\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n static add(x, y) {\\n return x + y;\\n }\\n}\\n```\\n\\n\\n\\n```python\\nfrom fable_library_js.reflection import (TypeInfo, class_type)\\n\\nclass MyClass:\\n @staticmethod\\n def add(x: int, y: int) -> int:\\n return x + y\\n```\\n\\n\\n\\nThat looks better and allows us to do the following in all 3 languages: `MyClass.add`.\\nThis is the basic design i chose to use for most user facing api. All F#/Fable code not easily usable from other languages is hidden behind a facade like this.\\n\\n### C# Compatibility\\n\\nStrangely enough allowing C# users the same ease of use as Python and JavaScript users is the hardest. \\nThis is because C# has some issues with F# *optional parameters* and F# *tuples*.\\n\\nIn F# we can define a function like this:\\n\\n```fsharp\\n[]\\ntype flowchart =\\n static member raw (txt: string) = FlowchartElement txt\\n static member id (txt: string) = FlowchartElement txt\\n static member node (id: string, ?name: string) : FlowchartElement = ...\\n\\nflowchart.node(\\"My id\\")\\nflowchart.node(\\"My id\\", \\"My name\\")\\n```\\n\\nUsing the F# function in C# will result in an error, when you try to do `flowchart.node(\\"My id\\")`, as `?name` is a `Microsoft.FSharp.Core.FSharpOption` without any default information.\\n\\nBy creating a C# access layer we can avoid this issue for C# users:\\n\\n:::info\\nThe C# extensions for optional parameters and tuples are taken from [Plotly.NET](https://github.com/plotly/Plotly.NET) with the help from my dear colleague [Kevin Schneider][KevinS].\\n:::\\n\\n\\n\\n```csharp\\npublic static class flowchart\\n{\\n public static FlowchartElement raw(string txt) => Siren.flowchart.raw(txt);\\n\\n public static FlowchartElement id(string txt) => Siren.flowchart.raw(txt);\\n\\n public static FlowchartElement node(string id, Optional name = default) =>\\n Siren.flowchart.node(id, name.ToOption());\\n //...\\n}\\n```\\n\\n\\n\\n```csharp\\n/// \\n/// Helper type for handling the special way the Plotly.NET core API uses generics.\\n/// In short, the problem arises because many optional parameters of Plotly.NET\'s core API are generics \\n/// with a type constraint for `IConvertible`. This means that these parameters can be both value and reference types \\n/// (e.g. `double` and `System.DateTime` both implement IConvertible).\\n/// If we now have a optional parameter of type `T? where T: IConvertible` the compiler will not allow this \\n/// without further type constrainst to eithe reference or value type.\\n/// This is a problem because we want to 1. allow both, and 2. have a reliable way of determining if the value was not set \\n/// because the F# API expects to be passed `Option.None` in that case.\\n/// There exist other workarounds like checking if the value is default or null, but that changes valid default values actually set to null as well.\\n/// \\n/// \\n/// The value to mark as optional\\n/// Wether or not the wrapped value is valid. This is used downstream to determine wether to wrap this value into `Option.Some` (if true) or `Option.None` (if false)\\npublic readonly record struct Optional(T Value, bool IsSome)\\n{\\n /// \\n /// \\n /// \\n /// \\n public static implicit operator Optional(T Value) => new(Value, true);\\n\\n}\\n/// \\n/// Extension methods for the `Optional` class\\n/// \\npublic static class OptionalExtensions\\n{\\n /// \\n /// Converts the `Optional` value to `Some(value)` if the value is valid, or `None` if it is not.\\n /// \\n /// \\n /// The `Optional` value to convert to a F# Option\\n /// opt converted to `Option`\\n static internal Microsoft.FSharp.Core.FSharpOption ToOption(this Optional opt) => opt.IsSome ? new(opt.Value) : Microsoft.FSharp.Core.FSharpOption.None;\\n}\\n```\\n\\n\\n\\nA similar issue arises with F# tuples:\\n\\n\\n\\n```fsharp\\n[]\\ntype flowchart =\\n static member stylesNode (nodeId: string, styles:#seq) = Flowchart.formatNodeStyles [nodeId] (List.ofSeq styles) |> FlowchartElement\\n```\\n\\n\\n\\n```csharp\\n/// \\n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\\n/// \\ninternal static class TupleExtensions\\n{\\n /// \\n /// Converts a tuple.\\n /// \\n internal static Tuple ToTuple(this ValueTuple t) => Tuple.Create(t.Item1, t.Item2);\\n}\\n```\\n\\n\\n\\n```csharp\\n/// \\n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\\n/// \\npublic static class flowchart\\n{\\n public static FlowchartElement stylesNode(string nodeId, (string, string)[] styles) =>\\n Siren.flowchart.stylesNode(nodeId, styles.Select(t => t.ToTuple()));\\n}\\n```\\n\\n\\n\\n#### Code generator helper\\n\\nThese incompatibilities are not only annoying but providing a consistently native C# experience, requires a wrapping for all apis.\\nTo make this easier, i created a code generator that takes a F# file and generates the C# wrapper for it. .. Or at least 95% of it. The rest is done by hand.\\n\\n
\\nCode Generator\\n
\\n```fsharp\\nopen System.Reflection\\n\\n[]\\nlet FSharpOptionDefault = \\"Optional\\"\\n\\nlet transformParameterTypeName (paramTypeName: string)=\\n match paramTypeName with\\n | \\"String\\" -> \\"string\\"\\n | \\"Int32\\" -> \\"int\\"\\n | \\"Double\\" -> \\"double\\"\\n | \\"FSharpOption`1\\" -> FSharpOptionDefault // this is not always true but a good approximation\\n | \\"Tuple`2\\" -> \\"(string,string)\\" // this is not always true but a good approximation\\n | \\"Boolean\\" -> \\"bool\\"\\n | _ -> paramTypeName\\n\\ntype ParameterInfo = {\\n Type: string\\n Name: string\\n} with\\n member this.FSharpParam =\\n match this with\\n | {Type = FSharpOptionDefault} -> this.Name + \\".ToOption()\\"\\n | _ -> this.Name\\n member this.CSharpParam =\\n match this with\\n | {Type = FSharpOptionDefault} -> sprintf \\"%s %s = default\\" this.Type this.Name\\n | _ -> sprintf \\"%s %s\\" this.Type this.Name\\n\\n static member create(typeName: string, name: string) =\\n {Type = transformParameterTypeName typeName; Name = name}\\n\\nlet generateCSharpCode<\'A>() =\\n\\n let t = typeof<\'A>\\n let members = t.GetMethods(BindingFlags.Static ||| BindingFlags.Public)\\n\\n let mutable csharpCode = sprintf \\"public static class %s\\\\n{\\\\n\\" t.Name\\n for m in members do\\n let methodName = \\n let name0 = m.Name\\n if name0.StartsWith(\\"get_\\") then\\n name0.Substring(4)\\n else\\n name0\\n let returnType = m.ReturnType.Name\\n let params0 = \\n m.GetParameters() \\n |> Array.map (fun p -> ParameterInfo.create(p.ParameterType.Name, p.Name))\\n let csharpParameters =\\n if params0.Length = 0 then\\n \\"\\"\\n else \\n params0\\n |> Array.map _.CSharpParam\\n |> String.concat(\\", \\")\\n |> fun s -> \\"(\\" + s + \\")\\"\\n \\n let fsharpParameters = \\n if params0.Length = 0 then\\n \\"\\"\\n else \\n params0 \\n |> Array.map _.FSharpParam\\n |> String.concat(\\", \\")\\n |> fun s -> \\"(\\" + s + \\")\\"\\n \\n let methodSignature = $\\"public static {transformParameterTypeName returnType} {methodName}{csharpParameters}\\"\\n let methodBody = \\n if methodName.StartsWith(\\"get_\\") then\\n let withoutGet = methodName.Substring(4)\\n $\\"return Siren.{t.Name}.{withoutGet};\\"\\n else\\n $\\" => Siren.{t.Name}.{methodName}{fsharpParameters};\\"\\n csharpCode <- csharpCode + $\\" {methodSignature}\\\\n {methodBody}\\\\n\\"\\n\\n csharpCode <- csharpCode + \\"}\\\\n\\"\\n csharpCode\\n\\nlet test() = \\n generateCSharpCode() // Here you can pass any type you want to generate C# code for\\n |> printfn \\"%A\\"\\n```\\n
\\n
\\n\\nThis is something i did not want to spend a lot of time on, so when i quickly wrote this and noticed it was able to create most of the C# code correctly.\\nI think improving this code to create everything perfectly would be awesome, but would have taken me longer thant fixing the few mistakes it makes by hand.\\n\\n#### Maintainability\\n\\nThis is a big issue. Whenever i update my f# api i must also update the c# wrapper.\\nChanges are mostly catched by the compiler but missings functions are not.\\n\\nThat is why i added unit tests to check the count and name of a c# and f# class and compare it for equality:\\n\\n```csharp\\npublic static class Utils\\n{\\n public static int GetMemberCount(Type type)\\n {\\n var members = type.GetMembers();\\n return members.Length;\\n }\\n\\n public static List GetMemberNameDifferences(Type type1, Type type2)\\n {\\n List differences = new List();\\n //transform string to lower\\n var type1Members = type1.GetMembers().Select(m => m.Name.ToLower());\\n var type2Members = type2.GetMembers().Select(m => m.Name.ToLower());\\n differences.AddRange(type1Members.Except(type2Members));\\n differences.AddRange(type2Members.Except(type1Members));\\n\\n return differences;\\n }\\n\\n public static void CompareClasses(Type csharpType, Type fsharpType)\\n {\\n int csharpMemberCount = GetMemberCount(csharpType);\\n int fsharpMemberCount = GetMemberCount(fsharpType);\\n List differences = GetMemberNameDifferences(fsharpType, csharpType);\\n\\n Assert.Empty(differences);\\n Assert.Equal(fsharpMemberCount, csharpMemberCount);\\n }\\n}\\n```\\n\\nThis at least helps me to catch missing functions in the c# wrapper, even if i still have to write them by hand.\\n\\n### Python/JavaScript import and Index files\\n\\nF# and C# use namespaces to organize code. I can have multiple files with the same namespace and access all functions simply by writing `open Siren`/`using Siren.Sea;`.\\n\\nIn Python and JavaScript this is not possible. Here imports happen on a file basis. So i needed to have a single file that imports all other files and exports them.\\n\\nLuckily this file can be created automatically!\\n\\n
\\nIndex File\\n
\\n\\n\\n```fsharp\\nmodule Index.Util\\n\\nopen System\\nopen System.IO\\nopen System.Text.RegularExpressions\\n\\ntype FileInformation = {\\n FilePath : string\\n Lines : string []\\n} with\\n static member create(filePath: string, lines: string []) = {\\n FilePath = filePath\\n Lines = lines\\n }\\n\\nlet getAllFiles(path: string, extension: string) = \\n let options = EnumerationOptions()\\n options.RecurseSubdirectories <- true\\n IO.Directory.EnumerateFiles(path,extension,options)\\n |> Seq.filter (fun s -> s.Contains(\\"fable_modules\\") |> not)\\n |> Array.ofSeq\\n\\nlet findClasses (rootPath: string) (cois: string []) (regexPattern: string -> string) (filePaths: seq) = \\n let files = [|\\n for fp in filePaths do\\n yield FileInformation.create(fp, System.IO.File.ReadAllLines (fp))\\n |]\\n let importStatements = ResizeArray()\\n let findClass (className: string) = \\n /// maybe set this as default if you do not want any whitelist\\n let classNameDefault = @\\"[a-zA-Z_0-9]\\"\\n let pattern = Regex.Escape(className) |> regexPattern\\n let regex = Regex(pattern)\\n let mutable found = false\\n let mutable result = None\\n let enum = files.GetEnumerator()\\n while not found && enum.MoveNext() do\\n let fileInfo = enum.Current :?> FileInformation\\n for line in fileInfo.Lines do\\n let m = regex.Match(line)\\n match m.Success with\\n | true -> \\n found <- true\\n result <- Some <| (className, IO.Path.GetRelativePath(rootPath,fileInfo.FilePath))\\n | false ->\\n ()\\n match result with\\n | None ->\\n failwithf \\"Unable to find %s\\" className\\n | Some r ->\\n importStatements.Add r\\n for coi in cois do findClass coi\\n importStatements\\n |> Array.ofSeq\\n\\nlet writeIndexFile (path: string) (fileName: string) (content: string) =\\n let filePath = Path.Combine(path, fileName)\\n File.WriteAllText(filePath, content)\\n```\\n\\n\\n\\n```fsharp\\nmodule Index.JS\\n\\nlet private getAllJsFiles (path: string) fileExtension = \\n Util.getAllFiles(path,$\\"*.{fileExtension}\\")\\n\\nlet private pattern (className: string) = sprintf @\\"^export class (?%s)+[\\\\s{].*({)?\\" className\\n\\nlet private findJsClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \\n Util.findClasses rootPath whiteList pattern filePaths\\n\\nopen System.Text\\n\\nlet private createImportStatements (info: (string*string) []) =\\n let sb = StringBuilder()\\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\\n for filePath, imports in importCollection do\\n let p = filePath.Replace(\\"\\\\\\\\\\",\\"/\\").Replace(\\"ts\\",\\"js\\")\\n sb.Append \\"export { \\" |> ignore\\n sb.AppendJoin(\\", \\", imports) |> ignore\\n sb.Append \\" } from \\" |> ignore\\n sb.Append (sprintf \\"\\\\\\"./%s\\\\\\"\\" p) |> ignore\\n sb.Append \\";\\" |> ignore\\n sb.AppendLine() |> ignore\\n sb.ToString()\\n\\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string [], fileExtension: string) =\\n getAllJsFiles rootPath fileExtension\\n |> findJsClasses rootPath whiteList\\n |> createImportStatements\\n |> Util.writeIndexFile rootPath fileName\\n\\nlet generate(rootPath: string) (ts: bool) = \\n let extension = if ts then \\"ts\\" else \\"js\\"\\n generateIndexfile(rootPath, $\\"index.{extension}\\", WhiteList.WhiteList, extension)\\n```\\n\\n\\n\\n```fsharp\\nlet private getAllJsFiles(path: string) = \\n Util.getAllFiles(path,\\"*.py\\")\\n\\nlet private pattern (className: string) = sprintf @\\"^class (?%s)+(\\\\(|:).*$\\" className\\n\\nlet private findPyClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \\n Util.findClasses rootPath whiteList pattern filePaths\\n\\nopen System.Text\\n\\nlet private createImportStatements (info: (string*string) []) =\\n let sb = StringBuilder()\\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\\n for filePath, imports in importCollection do\\n let p = filePath |> Path.GetFileNameWithoutExtension\\n sb.Append (sprintf \\"from .%s import \\" p) |> ignore\\n sb.AppendJoin(\\", \\", imports) |> ignore\\n sb.AppendLine() |> ignore\\n sb.ToString()\\n\\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string []) =\\n getAllJsFiles(rootPath)\\n |> findPyClasses rootPath whiteList\\n |> createImportStatements\\n |> Util.writeIndexFile rootPath fileName\\n\\nlet generate(rootPath: string) (name: string) = \\n // code to make camelcase to snakecase\\n /// This is because we currently snake_case everything that does not start with a capital letter\\n let camelCaseToSnakeCase (str: string) = \\n if Char.IsUpper str.[0] then\\n str \\n else\\n str \\n |> Seq.fold (fun (acc: string) c -> \\n if Char.IsUpper c then \\n acc + \\"_\\" + string (Char.ToLower c) \\n else \\n acc + string c\\n ) \\"\\"\\n let snake_case_white_list = WhiteList.WhiteList |> Array.map camelCaseToSnakeCase\\n generateIndexfile(rootPath, name, snake_case_white_list)\\n```\\n\\n\\n```fsharp\\nmodule Index.WhiteList\\n\\nlet WhiteList = [|\\n \\"SirenElement\\"\\n // ThemeVaruiables\\n \\"themeVariable\\"\\n \\"quadrantTheme\\"; \\n \\"gitTheme\\"; \\n \\"timelineTheme\\"; \\n \\"xyChartTheme\\"; \\n \\"pieTheme\\";\\n // Config\\n \\"graphConfig\\";\\n \\"flowchartConfig\\"; \\n \\"sequenceConfig\\"; \\n \\"ganttConfig\\"; \\n \\"journeyConfig\\"; \\n \\"timelineConfig\\"; \\n \\"classConfig\\"; \\n \\"stateConfig\\"; \\n \\"erConfig\\"; \\n \\"quadrantChartConfig\\"; \\n \\"pieConfig\\"; \\n \\"sankeyConfig\\"; \\n \\"xyChartConfig\\"; \\n \\"mindmapConfig\\"; \\n \\"gitGraphConfig\\"; \\n \\"requirementConfig\\"; \\n // Graphs and Helpers\\n \\"formatting\\"; \\n \\"direction\\"; \\n \\"flowchart\\"; \\n \\"notePosition\\"; \\n \\"sequence\\"; \\n \\"classMemberVisibility\\"; \\n \\"classMemberClassifier\\"; \\n \\"classDirection\\"; \\n \\"classCardinality\\"; \\n \\"classRltsType\\"; \\n \\"classDiagram\\"; \\n \\"stateDiagram\\"; \\n \\"erKey\\"; \\n \\"erCardinality\\"; \\n \\"erDiagram\\"; \\n \\"journey\\"; \\n \\"ganttTime\\"; \\n \\"ganttTags\\"; \\n \\"ganttUnit\\"; \\n \\"gantt\\"; \\n \\"pieChart\\"; \\n \\"quadrant\\"; \\n \\"rqRisk\\"; \\n \\"rqMethod\\"; \\n \\"requirement\\"; \\n \\"gitType\\"; \\n \\"git\\"; \\n \\"mindmap\\"; \\n \\"timeline\\"; \\n \\"sankey\\"; \\n \\"xyChart\\"; \\n \\"block\\"; \\n \\"theme\\"; \\n \\"siren\\"; \\n|]\\n```\\n\\n\\n\\n
\\n
\\n\\nThe resulting files look like this:\\n\\n\\n\\n```js\\nexport { SirenElement } from \\"./SirenTypes.js\\";\\nexport { themeVariable, quadrantTheme, gitTheme, timelineTheme, xyChartTheme, pieTheme } from \\"./ThemeVariables.js\\";\\nexport { graphConfig, flowchartConfig, sequenceConfig, ganttConfig, journeyConfig, timelineConfig, classConfig, stateConfig, erConfig, quadrantChartConfig, pieConfig, sankeyConfig, xyChartConfig, mindmapConfig, gitGraphConfig, requirementConfig } from \\"./Config.js\\";\\nexport { formatting, direction, flowchart, notePosition, sequence, classMemberVisibility, classMemberClassifier, classDirection, classCardinality, classRltsType, classDiagram, stateDiagram, erKey, erCardinality, erDiagram, journey, ganttTime, ganttTags, ganttUnit, gantt, pieChart, quadrant, rqRisk, rqMethod, requirement, gitType, git, mindmap, timeline, sankey, xyChart, block, theme, siren } from \\"./Siren.js\\";\\n```\\n\\n\\n```py\\nfrom .siren_types import SirenElement\\nfrom .theme_variables import theme_variable, quadrant_theme, git_theme, timeline_theme, xy_chart_theme, pie_theme\\nfrom .config import graph_config, flowchart_config, sequence_config, gantt_config, journey_config, timeline_config, class_config, state_config, er_config, quadrant_chart_config, pie_config, sankey_config, xy_chart_config, mindmap_config, git_graph_config, requirement_config\\nfrom .siren import formatting, direction, flowchart, note_position, sequence, class_member_visibility, class_member_classifier, class_direction, class_cardinality, class_rlts_type, class_diagram, state_diagram, er_key, er_cardinality, er_diagram, journey, gantt_time, gantt_tags, gantt_unit, gantt, pie_chart, quadrant, rq_risk, rq_method, requirement, git_type, git, mindmap, timeline, sankey, xy_chart, block, theme, siren\\n```\\n\\n\\n\\n:::info\\n\\nPython packages will default import whatever is in the `__init__.py` file. So we must simply name the index file for python as such (at least for publishing).\\n\\n:::\\n\\n## Publish\\n\\nAs soon as we get to the publishing step everything is back to standard for the respective languages.\\n\\n### .NET\\n\\nThe easiest. We already have the required project files, no need to transpile. So we can simply use `dotnet pack` to create .nupkg files. \\nThen `dotnet nuget push` to push to [nuget.org](https://www.nuget.org).\\n\\n\\n:::warning\\nThese are not the exact CLI args used. Details can be found in the build project in the GitHub repository.\\n:::\\n\\n### Pypi\\n\\nAlso quite easy. Transpile f# code to python code, copy `pyproject.toml` and `README.md` into the `dist` folder and create `index.py` file. \\nRun `python -m poetry build` to create a publishing files. Then publish files with `python -m poetry publish`\\n\\n:::warning\\nThese are not the exact CLI args used. Details can be found in the build project in the GitHub repository.\\n:::\\n\\n### JavaScript (+Types)\\n\\nFor this i followed the Example of [Better Typed than Sorry](https://fable.io/blog/2023/2023-04-20-Better_Typed_than_Sorry.html) by [Alfonso Garc\xeda-Caro](https://github.com/alfonsogarciacaro).\\n\\nTranspile F# to TypeScript, then use `tsc` to transpile TypeScript to JavaScript and type information files (`*.d.ts`). Add the `index.js` file to the `dist folder`\\nThen publish to npm with `npm publish`.\\n\\n:::warning\\nThese are not the exact CLI args used. Details can be found in the build project in the GitHub repository.\\n:::\\n\\n## Deep Dive\\n\\nFrom here on are some additional issues i encountered during development.\\n\\n### Overloads\\n\\nThe concept of allowing different inputs for the same function exists in f#, as well as python and javascript. \\nBy using the `[]` attribute we are not longer allowed to use standard f# overloads. \\nBecause JavaScript does not have the same kind of type interference it is unable to recognice which function should be invoked:\\n\\n\\n\\n```fsharp\\nopen Fable.Core\\n\\n[]\\ntype MyClass =\\n static member add (x: int, y: int) = x * y // this should be invoked\\n static member add (x: string, y: string) = x + y\\n\\nlet result = MyClass.add(10, 20)\\n\\nprintfn \\"%A\\" result\\n```\\n\\n\\n\\n```js\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\nimport { printf, toConsole } from \\"fable-library-js/String.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n static add(x, y) {\\n return x * y;\\n }\\n static add(x, y) { // This shadows the function above and is invoked\\n return x + y;\\n }\\n}\\n\\nexport const result = MyClass.add(10, 20); // This will return 30\\n\\ntoConsole(printf(\\"%A\\"))(result);\\n\\n```\\n\\n\\n\\n#### Erased Unions\\n\\nIt is possible to imitate js overload behaviour by using [erased unions](https://fable.io/docs/javascript/features.html#erased-unions).\\nHere we use a Fable provides discriminate union called ``U2``(``U3``, ``U4``...). After transpilation it is replaced by a js type check.\\n\\n\\n\\n```fsharp\\nopen Fable.Core\\nopen Fable.Core.JsInterop\\n\\n[]\\ntype MyClass =\\n static member test (arg: U2) = \\n match arg with\\n | U2.Case1 s -> printfn \\"This is a string: %s\\" s\\n | U2.Case2 i -> printfn \\"This is a integer: %i\\" i\\n\\nlet result = MyClass.test(U2.Case2 10) // or MyClass.test(!^10)\\n\\nprintfn \\"%A\\" result\\n```\\n\\n\\n\\n```js\\nimport { printf, toConsole } from \\"fable-library-js/String.js\\";\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n static test(arg) {\\n if (typeof arg === \\"number\\") {\\n const i = arg;\\n toConsole(printf(\\"This is a integer: %i\\"))(i);\\n }\\n else {\\n const s = arg;\\n toConsole(printf(\\"This is a string: %s\\"))(s);\\n }\\n }\\n}\\n\\nexport const result = MyClass.test(10);\\n\\ntoConsole(printf(\\"%A\\"))(); // This is a integer: 10\\n```\\n\\n\\n\\nAs you can see, this looks really nice in JavaScript, but is cumbersome to use in F#. \\n\\nIt would be possible to do do both and use f# overloads and shadow them with the erased union. But this would add more additional maintainance work.\\n\\n:::info[Compiler Statements]\\nWe can use `#if FABLE_COMPILER ... #else ... #endif` syntax to include code only in certain compiler states. \\n\\nIn the following example the erased union is only used (and accessible!) when the code is transpiled by Fable to JavaScript.\\n:::\\n\\n```fsharp\\n[]\\ntype MyClass =\\n// highlight-next-line\\n#if FABLE_COMPILER_JAVASCRIPT\\n static member test (arg: U2) = \\n match arg with\\n | U2.Case1 s -> printfn \\"This is a string: %s\\" s\\n | U2.Case2 i -> printfn \\"This is a integer: %i\\" i\\n// highlight-next-line\\n#else\\n static member test(arg: int) =\\n printfn \\"This is a integer: %i\\" arg\\n static member test(arg: string) =\\n printfn \\"This is a string: %s\\" arg\\n// highlight-next-line\\n#endif\\n\\nlet result = MyClass.test(10)\\n```\\n\\nThis would allow us to use the erased union only in JavaScript and use the F# overloads in F#. But i have not investigated how this would work for python \ud83d\ude05.\\n\\nDue to the additional workload i decided to avoid using overloads in the api. Instead i tried finding the core functions and functions, which allow additional inputs with a different name:\\n\\n```fsharp\\ntype sequence =\\n static member note(id: string, text: string, ?notePosition: NotePosition) = //..\\n static member noteSpanning(id1: string, id2, text: string, ?notePosition: NotePosition) = //..\\n```\\n\\n### JavaScript optional parameters\\n\\nUsing functions with multiple optional parameters is easily done in F#, C# and Python, but can get quite annoying in JavaScript:\\n\\n\\n\\n```js\\n// tripple null ...\\nrequirement.requirement(\\"my id\\", null, null, null, rqMethod.test)\\n```\\n\\n\\n\\n```fsharp\\n// easy!\\nrequirement.requirement(\\"My Id\\", rqMethod = rqMethod.analysis)\\n```\\n\\n\\n\\n```python\\n# easy!\\nrequirement.requirement(\\"My Id\\", rq_method = rq_method.analysis)\\n```\\n\\n\\n\\n```csharp\\n// easy!\\nRequirement.requirement(\\"My Id\\", rqMethod: rqMethod.analysis)\\n```\\n\\n\\n\\nThe JavaScript native approach would be using an object with only the values you want to set.\\nThere is even a way to tell Fable to transpile parameters as object using the `[ParamObject]` attribute.\\n\\n\\n\\n```fsharp\\nopen Fable.Core\\nopen Fable.Core.JsInterop\\n\\n[]\\ntype MyClass =\\n [] // Start creating obj from params at index 1\\n static member test (name: int, ?id: string, ?text: string, ?rqRisk: string, ?rqMethod: string) =\\n 0\\n\\n\\nMyClass.test(10, rqRisk = \\"Hello\\")\\nMyClass.test(10)\\n```\\n\\n\\n\\n```js\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n static test(name, { id, text, rqRisk, rqMethod }) {\\n return 0;\\n }\\n}\\n\\nMyClass.test(10, {\\n rqRisk: \\"Hello\\",\\n});\\n\\nMyClass.test(10, {}); // Oh oh. Why an empty object?\\n```\\n\\n\\n\\nAs you can see adding *no* optional parameters requires an empty object, as Fable checks if the value at object key xyz is null and not if the object is null. \\nWithout the empty object the function would throw an error, whenever any of the optional parameters is referenced in the function. (Bad example as i just return 0).\\n\\n### Member Names \\n\\nDifferent languages have different expectations for member names. Aside from styling best practises, there are some things that are not possible in all languages.\\n\\n:::info\\nF# typically uses PascalCase for class names and camelCase for member names. \\nFor easier usage i ignore this rule and used camelCase for everything.\\n:::\\n\\n#### F# reserved keywords\\n\\nThe first issue i encountered where reserved keywords in F#.\\n\\nFor example ` classDiagram.``class`` `. *\\"class\\"* is a reserved keyword which is not allowed in F#. \\nThe standard solution is wrapping the name in backticks. But at least for me on VisualStudio Community this resulted in issues with my auto complete.\\nThis resulted in me handling this issue inconsistently. The issues i encountered were mostly in (optional) parameters, which is why i changed their names to PascalCase:\\n\\nFor members i mostly stayed true to the backtick syntax.\\n\\n```fsharp\\n[]\\ntype classMemberClassifier =\\n // abstract is a reserved keyword\\n static member Abstract = ClassMemberClassifier.Abstract\\n // static is a reserved keyword\\n static member Static = ClassMemberClassifier.Static\\n static member custom str = ClassMemberClassifier.Custom str\\n\\n[]\\ntype classDiagram =\\n static member ``class`` (id: string, members: #seq) = \\n```\\n\\nThe C# wrapper used the C# best practise syntax `@class`, which worked fine for me.\\n\\n#### C# - Member name = enclosing type\\n\\nI encountered this issue first for `block.block`. In C# member names are not allowed to be the same as the enclosing type.\\n\\nAs i am not a very experienced C# developer, i am still rather undecided on how to handle this issue. \\nSo far I have been using `Block.block`, as I am thinking about using PascalCase for all classes in C#. And if only to mute the warnings in VS Community.\\n\\nIf you have a strong opinion about this topic, please let me know! I am interested in hearing your thoughts.\\n\\n#### Transpiled names\\n\\nAnd back to `classDiagram.``class`` `. While JavaScript does not seem to care about this topic to much, Python does.\\n\\nJavaScript gives us the best result, the F# backtick syntax is transpiled to a simple camelCase name `classDiagram.class`.\\n\\nPython on the other hand has `class` also as reserved keyword. Fable transpiles it to `classDiagram.class_`. \\nWhich raises the question if i should simply apply this syntax to all cases with naming problems.\\n\\n### Docs/Native Test Maintainance\\n\\nThe core library + C# wrapper were done rather quickly. I can also recycle my F# unit tests to check if transpilation works as expected, using [Fable.Pyxpecto](https://github.com/Freymaurer/Fable.Pyxpecto).\\n\\nBut testing correct native accessibillity and writing docs (showcasing the test cases) was the most time consuming part and something i am not happy with.\\n\\nHere are some ideas on how to improve this:\\n\\n- Theoretically, i could use the transpiled tests for docs. But i still have to remove the Fable specific helper functions and replace them with native ones.\\n- *[Kevin S.][KevinS]* had an idea, repurposing jupyter notebooks for docs and testing. To at least unify the testing and docs. \\n\\nIf you have any ideas on how to improve this, please let me know!\\n\\n\\n{/* References */}\\n[Fable]: https://fable.io\\n[REPL]: https://fable.io/repl/\\n[KevinS]: https://github.com/kMutagene\\n[ncave]: https://github.com/ncave"}]}')}}]); \ No newline at end of file diff --git a/assets/js/5e06daff.85b2920d.js b/assets/js/5e06daff.85b2920d.js deleted file mode 100644 index 3736077..0000000 --- a/assets/js/5e06daff.85b2920d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2338],{6426:e=>{e.exports=JSON.parse('{"blogPosts":[{"id":"Library Design","metadata":{"permalink":"/Siren/blog/Library Design","editUrl":"https://github.com/Freymaurer/Siren/tree/main/docs/blog/2024-05-06-fable-library-design.mdx","source":"@site/blog/2024-05-06-fable-library-design.mdx","title":"Library Design","description":"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#.","date":"2024-05-06T00:00:00.000Z","tags":[{"label":"fable","permalink":"/Siren/blog/tags/fable"},{"label":"library","permalink":"/Siren/blog/tags/library"},{"label":"api","permalink":"/Siren/blog/tags/api"},{"label":"design","permalink":"/Siren/blog/tags/design"},{"label":"helper","permalink":"/Siren/blog/tags/helper"}],"readingTime":23.445,"hasTruncateMarker":false,"authors":[{"name":"Kevin Frey","title":"Maintainer of Siren","url":"https://github.com/Freymaurer","imageURL":"https://github.com/Freymaurer.png","key":"freyk"}],"frontMatter":{"slug":"Library Design","title":"Library Design","authors":"freyk","tags":["fable","library","api","design","helper"]},"unlisted":false},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n\\nSiren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#.\\n\\n:::tip[Fable.Multiverse]\\n\\nTo make it as easy as possible to create a Fable library that publishes in multiple languages, I created a template called [Fable.Multiverse](https://github.com/Freymaurer/Fable.Multiverse) :tada:\\n\\n:::\\n\\n## Idea \ud83d\udca1\\n\\nDuring a hackathon for our research data management consortium, we were discussing ideas for visualizing graph like structures in a way that allows easy gitlab integration and can be understood and used by any level of user. The idea we pursued was to add `.md` files with mermaid graphs for an easy overview. So why not write a domain specific language for mermaid graphs to make creation of such graphs easier and more error proof. Good idea, but what programming language should we use? In our consortium we have several groups, some using JavaScript, some Python, some (us) .NET or more specifically F#. Because i already have quite some experience using [Fable][Fable], i did the mental checklist to see if it would be a good fit for this project.\\n\\n- [x] *Does not need any dependencies.* Mermaid graphs, are mostly YAML, so no complex syntax.\\n- [x] *Does not require IO interaction.* We can simply focus on writing our mermaid graph as string and allow the user to do whatever they want with it.\\n- [x] *Only Fable compatible languages needed.* We are already very happy offering such a tool in Python, JavaScript and F#.\\n\\n.. and thats it \ud83c\udf89 So we can start developing a libary with one codebase for 4 [5] languages.\\n\\n## What is Fable?\\n\\nFable is a F# to X transpiler. It started out targeting only JavaScript, using a naming reference to the popular [Babel](https://babeljs.io) JavaScript transpiler. Now Fable aims to support multiple languages, all in different states. At the time of writing, the offical Fable docs state the following:\\n\\n| Language | Status | \\n|---|---|\\n| JavaScript | Stable |\\n| TypeScript |\\tStable| \\n| Dart |\\tBeta| \\n| Python |\\tBeta| \\n| Rust |\\tAlpha| \\n| PHP |\\tExperimental| \\n\\n### Benefits\\n\\n- **Type Safety**. F# is a statically typed language, which means that the compiler can catch many errors before they even happen.\\n- **Lightweight Syntax**. F# has a very lightweight syntax, which makes it easy to read and write. It does not require a lot of boilerplate code and you can get right into the meat of your program.\\n- **Testing(?)**. Main codebase is written in F# as well as most tests. This allows us to also transpile the tests to other languages and run them there. This is a big advantage, as we can be sure that the tests are the same in all languages.\\n\\n:::note[But why the question mark behind testing?]\\n\\nBecause we can recycle the tests, to ensure correct functionality, but we still must test if the library can be used from all supported languages without hurdles.\\n:::\\n\\n## API Design\\n\\nTo make the code look and feel as native as possible in all languages, there are some things we need to consider. But first let us have a look at fable transpiled code.\\n\\n:::info\\nThe following code will use the [Fable REPL][REPL] to transpile code for easy showcasing!\\n:::\\n\\n\\n\\n\\n```fsharp\\nlet helloWorld = printfn \\"Hello World\\"\\n```\\n\\n\\n\\n```js\\nimport { printf, toConsole } from \\"fable-library-js/String.js\\";\\n\\nexport const helloWorld = toConsole(printf(\\"Hello World\\"));\\n```\\n\\n\\n\\n```python\\nfrom fable_library_js.string import (to_console, printf)\\n\\nhello_world: None = to_console(printf(\\"Hello World\\"))\\n```\\n\\n\\n\\nWe can already notice some things:\\n\\n- Fable tries to transpile into native syntax, so for example snake_case in Python and camelCase in JavaScript\\n- Fable has some wrappers for functions which might have native equivalents. In F# `printfn` is used to print to the console, in JavaScript `console.log` and in Python `print`. But Fable uses their own printf function to ensure 100% correct f# transpilation.\\n\\nNext we will have a look at a class with some member functions.\\n\\n\\n\\n```fsharp\\ntype MyClass =\\n static member add (x: int) (y: int) = x + y\\n```\\n\\n\\n\\n```js\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n}\\n\\nexport function MyClass_$reflection() {\\n return class_type(\\"Test.MyClass\\", undefined, MyClass);\\n}\\n\\nexport function MyClass_add(x, y) {\\n return x + y;\\n}\\n```\\n\\n\\n\\n```python\\nfrom fable_library_js.reflection import (TypeInfo, class_type)\\n\\ndef _expr0() -> TypeInfo:\\n return class_type(\\"Test.MyClass\\", None, MyClass)\\n\\n\\nclass MyClass:\\n ...\\n\\nMyClass_reflection = _expr0\\n\\ndef MyClass_add(x: int, y: int) -> int:\\n return x + y\\n```\\n\\n\\n\\nOh no, this does not look good. Fable does a thing called [*name mangling*](https://fable.io/docs/javascript/features.html#name-mangling). \\nHave a look at the offical docs for a deeper view on this topic.\\nFor now its enought to know, this is done to allow overloading functions in F#.\\n\\n### Using `[]`\\n\\nBut we can tell Fable that we know what we are doing and ignore name mangling.\\n\\n\\n\\n```fsharp\\nopen Fable.Core\\n\\n[]\\ntype MyClass =\\n static member add (x: int) (y: int) = x + y\\n```\\n\\n\\n\\n```js\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n static add(x, y) {\\n return x + y;\\n }\\n}\\n```\\n\\n\\n\\n```python\\nfrom fable_library_js.reflection import (TypeInfo, class_type)\\n\\nclass MyClass:\\n @staticmethod\\n def add(x: int, y: int) -> int:\\n return x + y\\n```\\n\\n\\n\\nThat looks better and allows us to do the following in all 3 languages: `MyClass.add`.\\nThis is the basic design i chose to use for most user facing api. All F#/Fable code not easily usable from other languages is hidden behind a facade like this.\\n\\n### C# Compatibility\\n\\nStrangely enough allowing C# users the same ease of use as Python and JavaScript users is the hardest. \\nThis is because C# has some issues with F# *optional parameters* and F# *tuples*.\\n\\nIn F# we can define a function like this:\\n\\n```fsharp\\n[]\\ntype flowchart =\\n static member raw (txt: string) = FlowchartElement txt\\n static member id (txt: string) = FlowchartElement txt\\n static member node (id: string, ?name: string) : FlowchartElement = ...\\n\\nflowchart.node(\\"My id\\")\\nflowchart.node(\\"My id\\", \\"My name\\")\\n```\\n\\nUsing the F# function in C# will result in an error, when you try to do `flowchart.node(\\"My id\\")`, as `?name` is a `Microsoft.FSharp.Core.FSharpOption` without any default information.\\n\\nBy creating a C# access layer we can avoid this issue for C# users:\\n\\n:::info\\nThe C# extensions for optional parameters and tuples are taken from [Plotly.NET](https://github.com/plotly/Plotly.NET) with the help from my dear colleague [Kevin Schneider][KevinS].\\n:::\\n\\n\\n\\n```csharp\\npublic static class flowchart\\n{\\n public static FlowchartElement raw(string txt) => Siren.flowchart.raw(txt);\\n\\n public static FlowchartElement id(string txt) => Siren.flowchart.raw(txt);\\n\\n public static FlowchartElement node(string id, Optional name = default) =>\\n Siren.flowchart.node(id, name.ToOption());\\n //...\\n}\\n```\\n\\n\\n\\n```csharp\\n/// \\n/// Helper type for handling the special way the Plotly.NET core API uses generics.\\n/// In short, the problem arises because many optional parameters of Plotly.NET\'s core API are generics \\n/// with a type constraint for `IConvertible`. This means that these parameters can be both value and reference types \\n/// (e.g. `double` and `System.DateTime` both implement IConvertible).\\n/// If we now have a optional parameter of type `T? where T: IConvertible` the compiler will not allow this \\n/// without further type constrainst to eithe reference or value type.\\n/// This is a problem because we want to 1. allow both, and 2. have a reliable way of determining if the value was not set \\n/// because the F# API expects to be passed `Option.None` in that case.\\n/// There exist other workarounds like checking if the value is default or null, but that changes valid default values actually set to null as well.\\n/// \\n/// \\n/// The value to mark as optional\\n/// Wether or not the wrapped value is valid. This is used downstream to determine wether to wrap this value into `Option.Some` (if true) or `Option.None` (if false)\\npublic readonly record struct Optional(T Value, bool IsSome)\\n{\\n /// \\n /// \\n /// \\n /// \\n public static implicit operator Optional(T Value) => new(Value, true);\\n\\n}\\n/// \\n/// Extension methods for the `Optional` class\\n/// \\npublic static class OptionalExtensions\\n{\\n /// \\n /// Converts the `Optional` value to `Some(value)` if the value is valid, or `None` if it is not.\\n /// \\n /// \\n /// The `Optional` value to convert to a F# Option\\n /// opt converted to `Option`\\n static internal Microsoft.FSharp.Core.FSharpOption ToOption(this Optional opt) => opt.IsSome ? new(opt.Value) : Microsoft.FSharp.Core.FSharpOption.None;\\n}\\n```\\n\\n\\n\\nA similar issue arises with F# tuples:\\n\\n\\n\\n```fsharp\\n[]\\ntype flowchart =\\n static member stylesNode (nodeId: string, styles:#seq) = Flowchart.formatNodeStyles [nodeId] (List.ofSeq styles) |> FlowchartElement\\n```\\n\\n\\n\\n```csharp\\n/// \\n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\\n/// \\ninternal static class TupleExtensions\\n{\\n /// \\n /// Converts a tuple.\\n /// \\n internal static Tuple ToTuple(this ValueTuple t) => Tuple.Create(t.Item1, t.Item2);\\n}\\n```\\n\\n\\n\\n```csharp\\n/// \\n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\\n/// \\npublic static class flowchart\\n{\\n public static FlowchartElement stylesNode(string nodeId, (string, string)[] styles) =>\\n Siren.flowchart.stylesNode(nodeId, styles.Select(t => t.ToTuple()));\\n}\\n```\\n\\n\\n\\n#### Code generator helper\\n\\nThese incompatibilities are not only annoying but providing a consistently native C# experience, requires a wrapping for all apis.\\nTo make this easier, i created a code generator that takes a F# file and generates the C# wrapper for it. .. Or at least 95% of it. The rest is done by hand.\\n\\n
\\nCode Generator\\n
\\n```fsharp\\nopen System.Reflection\\n\\n[]\\nlet FSharpOptionDefault = \\"Optional\\"\\n\\nlet transformParameterTypeName (paramTypeName: string)=\\n match paramTypeName with\\n | \\"String\\" -> \\"string\\"\\n | \\"Int32\\" -> \\"int\\"\\n | \\"Double\\" -> \\"double\\"\\n | \\"FSharpOption`1\\" -> FSharpOptionDefault // this is not always true but a good approximation\\n | \\"Tuple`2\\" -> \\"(string,string)\\" // this is not always true but a good approximation\\n | \\"Boolean\\" -> \\"bool\\"\\n | _ -> paramTypeName\\n\\ntype ParameterInfo = {\\n Type: string\\n Name: string\\n} with\\n member this.FSharpParam =\\n match this with\\n | {Type = FSharpOptionDefault} -> this.Name + \\".ToOption()\\"\\n | _ -> this.Name\\n member this.CSharpParam =\\n match this with\\n | {Type = FSharpOptionDefault} -> sprintf \\"%s %s = default\\" this.Type this.Name\\n | _ -> sprintf \\"%s %s\\" this.Type this.Name\\n\\n static member create(typeName: string, name: string) =\\n {Type = transformParameterTypeName typeName; Name = name}\\n\\nlet generateCSharpCode<\'A>() =\\n\\n let t = typeof<\'A>\\n let members = t.GetMethods(BindingFlags.Static ||| BindingFlags.Public)\\n\\n let mutable csharpCode = sprintf \\"public static class %s\\\\n{\\\\n\\" t.Name\\n for m in members do\\n let methodName = \\n let name0 = m.Name\\n if name0.StartsWith(\\"get_\\") then\\n name0.Substring(4)\\n else\\n name0\\n let returnType = m.ReturnType.Name\\n let params0 = \\n m.GetParameters() \\n |> Array.map (fun p -> ParameterInfo.create(p.ParameterType.Name, p.Name))\\n let csharpParameters =\\n if params0.Length = 0 then\\n \\"\\"\\n else \\n params0\\n |> Array.map _.CSharpParam\\n |> String.concat(\\", \\")\\n |> fun s -> \\"(\\" + s + \\")\\"\\n \\n let fsharpParameters = \\n if params0.Length = 0 then\\n \\"\\"\\n else \\n params0 \\n |> Array.map _.FSharpParam\\n |> String.concat(\\", \\")\\n |> fun s -> \\"(\\" + s + \\")\\"\\n \\n let methodSignature = $\\"public static {transformParameterTypeName returnType} {methodName}{csharpParameters}\\"\\n let methodBody = \\n if methodName.StartsWith(\\"get_\\") then\\n let withoutGet = methodName.Substring(4)\\n $\\"return Siren.{t.Name}.{withoutGet};\\"\\n else\\n $\\" => Siren.{t.Name}.{methodName}{fsharpParameters};\\"\\n csharpCode <- csharpCode + $\\" {methodSignature}\\\\n {methodBody}\\\\n\\"\\n\\n csharpCode <- csharpCode + \\"}\\\\n\\"\\n csharpCode\\n\\nlet test() = \\n generateCSharpCode() // Here you can pass any type you want to generate C# code for\\n |> printfn \\"%A\\"\\n```\\n
\\n
\\n\\nThis is something i did not want to spend a lot of time on, so when i quickly wrote this and noticed it was able to create most of the C# code correctly.\\nI think improving this code to create everything perfectly would be awesome, but would have taken me longer thant fixing the few mistakes it makes by hand.\\n\\n#### Maintainability\\n\\nThis is a big issue. Whenever i update my f# api i must also update the c# wrapper.\\nChanges are mostly catched by the compiler but missings functions are not.\\n\\nThat is why i added unit tests to check the count and name of a c# and f# class and compare it for equality:\\n\\n```csharp\\npublic static class Utils\\n{\\n public static int GetMemberCount(Type type)\\n {\\n var members = type.GetMembers();\\n return members.Length;\\n }\\n\\n public static List GetMemberNameDifferences(Type type1, Type type2)\\n {\\n List differences = new List();\\n //transform string to lower\\n var type1Members = type1.GetMembers().Select(m => m.Name.ToLower());\\n var type2Members = type2.GetMembers().Select(m => m.Name.ToLower());\\n differences.AddRange(type1Members.Except(type2Members));\\n differences.AddRange(type2Members.Except(type1Members));\\n\\n return differences;\\n }\\n\\n public static void CompareClasses(Type csharpType, Type fsharpType)\\n {\\n int csharpMemberCount = GetMemberCount(csharpType);\\n int fsharpMemberCount = GetMemberCount(fsharpType);\\n List differences = GetMemberNameDifferences(fsharpType, csharpType);\\n\\n Assert.Empty(differences);\\n Assert.Equal(fsharpMemberCount, csharpMemberCount);\\n }\\n}\\n```\\n\\nThis at least helps me to catch missing functions in the c# wrapper, even if i still have to write them by hand.\\n\\n### Python/JavaScript import and Index files\\n\\nF# and C# use namespaces to organize code. I can have multiple files with the same namespace and access all functions simply by writing `open Siren`/`using Siren.Sea;`.\\n\\nIn Python and JavaScript this is not possible. Here imports happen on a file basis. So i needed to have a single file that imports all other files and exports them.\\n\\nLuckily this file can be created automatically!\\n\\n
\\nIndex File\\n
\\n\\n\\n```fsharp\\nmodule Index.Util\\n\\nopen System\\nopen System.IO\\nopen System.Text.RegularExpressions\\n\\ntype FileInformation = {\\n FilePath : string\\n Lines : string []\\n} with\\n static member create(filePath: string, lines: string []) = {\\n FilePath = filePath\\n Lines = lines\\n }\\n\\nlet getAllFiles(path: string, extension: string) = \\n let options = EnumerationOptions()\\n options.RecurseSubdirectories <- true\\n IO.Directory.EnumerateFiles(path,extension,options)\\n |> Seq.filter (fun s -> s.Contains(\\"fable_modules\\") |> not)\\n |> Array.ofSeq\\n\\nlet findClasses (rootPath: string) (cois: string []) (regexPattern: string -> string) (filePaths: seq) = \\n let files = [|\\n for fp in filePaths do\\n yield FileInformation.create(fp, System.IO.File.ReadAllLines (fp))\\n |]\\n let importStatements = ResizeArray()\\n let findClass (className: string) = \\n /// maybe set this as default if you do not want any whitelist\\n let classNameDefault = @\\"[a-zA-Z_0-9]\\"\\n let pattern = Regex.Escape(className) |> regexPattern\\n let regex = Regex(pattern)\\n let mutable found = false\\n let mutable result = None\\n let enum = files.GetEnumerator()\\n while not found && enum.MoveNext() do\\n let fileInfo = enum.Current :?> FileInformation\\n for line in fileInfo.Lines do\\n let m = regex.Match(line)\\n match m.Success with\\n | true -> \\n found <- true\\n result <- Some <| (className, IO.Path.GetRelativePath(rootPath,fileInfo.FilePath))\\n | false ->\\n ()\\n match result with\\n | None ->\\n failwithf \\"Unable to find %s\\" className\\n | Some r ->\\n importStatements.Add r\\n for coi in cois do findClass coi\\n importStatements\\n |> Array.ofSeq\\n\\nlet writeIndexFile (path: string) (fileName: string) (content: string) =\\n let filePath = Path.Combine(path, fileName)\\n File.WriteAllText(filePath, content)\\n```\\n\\n\\n\\n```fsharp\\nmodule Index.JS\\n\\nlet private getAllJsFiles (path: string) fileExtension = \\n Util.getAllFiles(path,$\\"*.{fileExtension}\\")\\n\\nlet private pattern (className: string) = sprintf @\\"^export class (?%s)+[\\\\s{].*({)?\\" className\\n\\nlet private findJsClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \\n Util.findClasses rootPath whiteList pattern filePaths\\n\\nopen System.Text\\n\\nlet private createImportStatements (info: (string*string) []) =\\n let sb = StringBuilder()\\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\\n for filePath, imports in importCollection do\\n let p = filePath.Replace(\\"\\\\\\\\\\",\\"/\\").Replace(\\"ts\\",\\"js\\")\\n sb.Append \\"export { \\" |> ignore\\n sb.AppendJoin(\\", \\", imports) |> ignore\\n sb.Append \\" } from \\" |> ignore\\n sb.Append (sprintf \\"\\\\\\"./%s\\\\\\"\\" p) |> ignore\\n sb.Append \\";\\" |> ignore\\n sb.AppendLine() |> ignore\\n sb.ToString()\\n\\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string [], fileExtension: string) =\\n getAllJsFiles rootPath fileExtension\\n |> findJsClasses rootPath whiteList\\n |> createImportStatements\\n |> Util.writeIndexFile rootPath fileName\\n\\nlet generate(rootPath: string) (ts: bool) = \\n let extension = if ts then \\"ts\\" else \\"js\\"\\n generateIndexfile(rootPath, $\\"index.{extension}\\", WhiteList.WhiteList, extension)\\n```\\n\\n\\n\\n```fsharp\\nlet private getAllJsFiles(path: string) = \\n Util.getAllFiles(path,\\"*.py\\")\\n\\nlet private pattern (className: string) = sprintf @\\"^class (?%s)+(\\\\(|:).*$\\" className\\n\\nlet private findPyClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \\n Util.findClasses rootPath whiteList pattern filePaths\\n\\nopen System.Text\\n\\nlet private createImportStatements (info: (string*string) []) =\\n let sb = StringBuilder()\\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\\n for filePath, imports in importCollection do\\n let p = filePath |> Path.GetFileNameWithoutExtension\\n sb.Append (sprintf \\"from .%s import \\" p) |> ignore\\n sb.AppendJoin(\\", \\", imports) |> ignore\\n sb.AppendLine() |> ignore\\n sb.ToString()\\n\\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string []) =\\n getAllJsFiles(rootPath)\\n |> findPyClasses rootPath whiteList\\n |> createImportStatements\\n |> Util.writeIndexFile rootPath fileName\\n\\nlet generate(rootPath: string) (name: string) = \\n // code to make camelcase to snakecase\\n /// This is because we currently snake_case everything that does not start with a capital letter\\n let camelCaseToSnakeCase (str: string) = \\n if Char.IsUpper str.[0] then\\n str \\n else\\n str \\n |> Seq.fold (fun (acc: string) c -> \\n if Char.IsUpper c then \\n acc + \\"_\\" + string (Char.ToLower c) \\n else \\n acc + string c\\n ) \\"\\"\\n let snake_case_white_list = WhiteList.WhiteList |> Array.map camelCaseToSnakeCase\\n generateIndexfile(rootPath, name, snake_case_white_list)\\n```\\n\\n\\n```fsharp\\nmodule Index.WhiteList\\n\\nlet WhiteList = [|\\n \\"SirenElement\\"\\n // ThemeVaruiables\\n \\"themeVariable\\"\\n \\"quadrantTheme\\"; \\n \\"gitTheme\\"; \\n \\"timelineTheme\\"; \\n \\"xyChartTheme\\"; \\n \\"pieTheme\\";\\n // Config\\n \\"graphConfig\\";\\n \\"flowchartConfig\\"; \\n \\"sequenceConfig\\"; \\n \\"ganttConfig\\"; \\n \\"journeyConfig\\"; \\n \\"timelineConfig\\"; \\n \\"classConfig\\"; \\n \\"stateConfig\\"; \\n \\"erConfig\\"; \\n \\"quadrantChartConfig\\"; \\n \\"pieConfig\\"; \\n \\"sankeyConfig\\"; \\n \\"xyChartConfig\\"; \\n \\"mindmapConfig\\"; \\n \\"gitGraphConfig\\"; \\n \\"requirementConfig\\"; \\n // Graphs and Helpers\\n \\"formatting\\"; \\n \\"direction\\"; \\n \\"flowchart\\"; \\n \\"notePosition\\"; \\n \\"sequence\\"; \\n \\"classMemberVisibility\\"; \\n \\"classMemberClassifier\\"; \\n \\"classDirection\\"; \\n \\"classCardinality\\"; \\n \\"classRltsType\\"; \\n \\"classDiagram\\"; \\n \\"stateDiagram\\"; \\n \\"erKey\\"; \\n \\"erCardinality\\"; \\n \\"erDiagram\\"; \\n \\"journey\\"; \\n \\"ganttTime\\"; \\n \\"ganttTags\\"; \\n \\"ganttUnit\\"; \\n \\"gantt\\"; \\n \\"pieChart\\"; \\n \\"quadrant\\"; \\n \\"rqRisk\\"; \\n \\"rqMethod\\"; \\n \\"requirement\\"; \\n \\"gitType\\"; \\n \\"git\\"; \\n \\"mindmap\\"; \\n \\"timeline\\"; \\n \\"sankey\\"; \\n \\"xyChart\\"; \\n \\"block\\"; \\n \\"theme\\"; \\n \\"siren\\"; \\n|]\\n```\\n\\n\\n\\n
\\n
\\n\\nThe resulting files look like this:\\n\\n\\n\\n```js\\nexport { SirenElement } from \\"./SirenTypes.js\\";\\nexport { themeVariable, quadrantTheme, gitTheme, timelineTheme, xyChartTheme, pieTheme } from \\"./ThemeVariables.js\\";\\nexport { graphConfig, flowchartConfig, sequenceConfig, ganttConfig, journeyConfig, timelineConfig, classConfig, stateConfig, erConfig, quadrantChartConfig, pieConfig, sankeyConfig, xyChartConfig, mindmapConfig, gitGraphConfig, requirementConfig } from \\"./Config.js\\";\\nexport { formatting, direction, flowchart, notePosition, sequence, classMemberVisibility, classMemberClassifier, classDirection, classCardinality, classRltsType, classDiagram, stateDiagram, erKey, erCardinality, erDiagram, journey, ganttTime, ganttTags, ganttUnit, gantt, pieChart, quadrant, rqRisk, rqMethod, requirement, gitType, git, mindmap, timeline, sankey, xyChart, block, theme, siren } from \\"./Siren.js\\";\\n```\\n\\n\\n```py\\nfrom .siren_types import SirenElement\\nfrom .theme_variables import theme_variable, quadrant_theme, git_theme, timeline_theme, xy_chart_theme, pie_theme\\nfrom .config import graph_config, flowchart_config, sequence_config, gantt_config, journey_config, timeline_config, class_config, state_config, er_config, quadrant_chart_config, pie_config, sankey_config, xy_chart_config, mindmap_config, git_graph_config, requirement_config\\nfrom .siren import formatting, direction, flowchart, note_position, sequence, class_member_visibility, class_member_classifier, class_direction, class_cardinality, class_rlts_type, class_diagram, state_diagram, er_key, er_cardinality, er_diagram, journey, gantt_time, gantt_tags, gantt_unit, gantt, pie_chart, quadrant, rq_risk, rq_method, requirement, git_type, git, mindmap, timeline, sankey, xy_chart, block, theme, siren\\n```\\n\\n\\n\\n:::info\\n\\nPython packages will default import whatever is in the `__init__.py` file. So we must simply name the index file for python as such (at least for publishing).\\n\\n:::\\n\\n## Publish\\n\\nAs soon as we get to the publishing step everything is back to standard for the respective languages.\\n\\n### .NET\\n\\nThe easiest. We already have the required project files, no need to transpile. So we can simply use `dotnet pack` to create .nupkg files. \\nThen `dotnet nuget push` to push to [nuget.org](https://www.nuget.org).\\n\\n\\n:::warning\\nThese are not the exact CLI args used. Details can be found in the build project in the GitHub repository.\\n:::\\n\\n### Pypi\\n\\nAlso quite easy. Transpile f# code to python code, copy `pyproject.toml` and `README.md` into the `dist` folder and create `index.py` file. \\nRun `python -m poetry build` to create a publishing files. Then publish files with `python -m poetry publish`\\n\\n:::warning\\nThese are not the exact CLI args used. Details can be found in the build project in the GitHub repository.\\n:::\\n\\n### JavaScript (+Types)\\n\\nFor this i followed the Example of [Better Typed than Sorry](https://fable.io/blog/2023/2023-04-20-Better_Typed_than_Sorry.html) by [Alfonso Garc\xeda-Caro](https://github.com/alfonsogarciacaro).\\n\\nTranspile F# to TypeScript, then use `tsc` to transpile TypeScript to JavaScript and type information files (`*.d.ts`). Add the `index.js` file to the `dist folder`\\nThen publish to npm with `npm publish`.\\n\\n:::warning\\nThese are not the exact CLI args used. Details can be found in the build project in the GitHub repository.\\n:::\\n\\n## Deep Dive\\n\\nFrom here on are some additional issues i encountered during development.\\n\\n### Overloads\\n\\nThe concept of allowing different inputs for the same function exists in f#, as well as python and javascript. \\nBy using the `[]` attribute we are not longer allowed to use standard f# overloads. \\nBecause JavaScript does not have the same kind of type interference it is unable to recognice which function should be invoked:\\n\\n\\n\\n```fsharp\\nopen Fable.Core\\n\\n[]\\ntype MyClass =\\n static member add (x: int, y: int) = x * y // this should be invoked\\n static member add (x: string, y: string) = x + y\\n\\nlet result = MyClass.add(10, 20)\\n\\nprintfn \\"%A\\" result\\n```\\n\\n\\n\\n```js\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\nimport { printf, toConsole } from \\"fable-library-js/String.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n static add(x, y) {\\n return x * y;\\n }\\n static add(x, y) { // This shadows the function above and is invoked\\n return x + y;\\n }\\n}\\n\\nexport const result = MyClass.add(10, 20); // This will return 30\\n\\ntoConsole(printf(\\"%A\\"))(result);\\n\\n```\\n\\n\\n\\n#### Erased Unions\\n\\nIt is possible to imitate js overload behaviour by using [erased unions](https://fable.io/docs/javascript/features.html#erased-unions).\\nHere we use a Fable provides discriminate union called ``U2``(``U3``, ``U4``...). After transpilation it is replaced by a js type check.\\n\\n\\n\\n```fsharp\\nopen Fable.Core\\nopen Fable.Core.JsInterop\\n\\n[]\\ntype MyClass =\\n static member test (arg: U2) = \\n match arg with\\n | U2.Case1 s -> printfn \\"This is a string: %s\\" s\\n | U2.Case2 i -> printfn \\"This is a integer: %i\\" i\\n\\nlet result = MyClass.test(U2.Case2 10) // or MyClass.test(!^10)\\n\\nprintfn \\"%A\\" result\\n```\\n\\n\\n\\n```js\\nimport { printf, toConsole } from \\"fable-library-js/String.js\\";\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n static test(arg) {\\n if (typeof arg === \\"number\\") {\\n const i = arg;\\n toConsole(printf(\\"This is a integer: %i\\"))(i);\\n }\\n else {\\n const s = arg;\\n toConsole(printf(\\"This is a string: %s\\"))(s);\\n }\\n }\\n}\\n\\nexport const result = MyClass.test(10);\\n\\ntoConsole(printf(\\"%A\\"))(); // This is a integer: 10\\n```\\n\\n\\n\\nAs you can see, this looks really nice in JavaScript, but is cumbersome to use in F#. \\n\\nIt would be possible to do do both and use f# overloads and shadow them with the erased union. But this would add more additional maintainance work.\\n\\n:::info[Compiler Statements]\\nWe can use `#if FABLE_COMPILER ... #else ... #endif` syntax to include code only in certain compiler states. \\n\\nIn the following example the erased union is only used (and accessible!) when the code is transpiled by Fable to JavaScript.\\n:::\\n\\n```fsharp\\n[]\\ntype MyClass =\\n// highlight-next-line\\n#if FABLE_COMPILER_JAVASCRIPT\\n static member test (arg: U2) = \\n match arg with\\n | U2.Case1 s -> printfn \\"This is a string: %s\\" s\\n | U2.Case2 i -> printfn \\"This is a integer: %i\\" i\\n// highlight-next-line\\n#else\\n static member test(arg: int) =\\n printfn \\"This is a integer: %i\\" arg\\n static member test(arg: string) =\\n printfn \\"This is a string: %s\\" arg\\n// highlight-next-line\\n#endif\\n\\nlet result = MyClass.test(10)\\n```\\n\\nThis would allow us to use the erased union only in JavaScript and use the F# overloads in F#. But i have not investigated how this would work for python \ud83d\ude05.\\n\\nDue to the additional workload i decided to avoid using overloads in the api. Instead i tried finding the core functions and functions, which allow additional inputs with a different name:\\n\\n```fsharp\\ntype sequence =\\n static member note(id: string, text: string, ?notePosition: NotePosition) = //..\\n static member noteSpanning(id1: string, id2, text: string, ?notePosition: NotePosition) = //..\\n```\\n\\n### JavaScript optional parameters\\n\\nUsing functions with multiple optional parameters is easily done in F#, C# and Python, but can get quite annoying in JavaScript:\\n\\n\\n\\n```js\\n// tripple null ...\\nrequirement.requirement(\\"my id\\", null, null, null, rqMethod.test)\\n```\\n\\n\\n\\n```fsharp\\n// easy!\\nrequirement.requirement(\\"My Id\\", rqMethod = rqMethod.analysis)\\n```\\n\\n\\n\\n```python\\n# easy!\\nrequirement.requirement(\\"My Id\\", rq_method = rq_method.analysis)\\n```\\n\\n\\n\\n```csharp\\n// easy!\\nRequirement.requirement(\\"My Id\\", rqMethod: rqMethod.analysis)\\n```\\n\\n\\n\\nThe JavaScript native approach would be using an object with only the values you want to set.\\nThere is even a way to tell Fable to transpile parameters as object using the `[ParamObject]` attribute.\\n\\n\\n\\n```fsharp\\nopen Fable.Core\\nopen Fable.Core.JsInterop\\n\\n[]\\ntype MyClass =\\n [] // Start creating obj from params at index 1\\n static member test (name: int, ?id: string, ?text: string, ?rqRisk: string, ?rqMethod: string) =\\n 0\\n\\n\\nMyClass.test(10, rqRisk = \\"Hello\\")\\nMyClass.test(10)\\n```\\n\\n\\n\\n```js\\nimport { class_type } from \\"fable-library-js/Reflection.js\\";\\n\\nexport class MyClass {\\n constructor() {\\n }\\n static test(name, { id, text, rqRisk, rqMethod }) {\\n return 0;\\n }\\n}\\n\\nMyClass.test(10, {\\n rqRisk: \\"Hello\\",\\n});\\n\\nMyClass.test(10, {}); // Oh oh. Why an empty object?\\n```\\n\\n\\n\\nAs you can see adding *no* optional parameters requires an empty object, as Fable checks if the value at object key xyz is null and not if the object is null. \\nWithout the empty object the function would throw an error, whenever any of the optional parameters is referenced in the function. (Bad example as i just return 0).\\n\\n### Member Names \\n\\nDifferent languages have different expectations for member names. Aside from styling best practises, there are some things that are not possible in all languages.\\n\\n:::info\\nF# typically uses PascalCase for class names and camelCase for member names. \\nFor easier usage i ignore this rule and used camelCase for everything.\\n:::\\n\\n#### F# reserved keywords\\n\\nThe first issue i encountered where reserved keywords in F#.\\n\\nFor example ` classDiagram.``class`` `. *\\"class\\"* is a reserved keyword which is not allowed in F#. \\nThe standard solution is wrapping the name in backticks. But at least for me on VisualStudio Community this resulted in issues with my auto complete.\\nThis resulted in me handling this issue inconsistently. The issues i encountered were mostly in (optional) parameters, which is why i changed their names to PascalCase:\\n\\nFor members i mostly stayed true to the backtick syntax.\\n\\n```fsharp\\n[]\\ntype classMemberClassifier =\\n // abstract is a reserved keyword\\n static member Abstract = ClassMemberClassifier.Abstract\\n // static is a reserved keyword\\n static member Static = ClassMemberClassifier.Static\\n static member custom str = ClassMemberClassifier.Custom str\\n\\n[]\\ntype classDiagram =\\n static member ``class`` (id: string, members: #seq) = \\n```\\n\\nThe C# wrapper used the C# best practise syntax `@class`, which worked fine for me.\\n\\n#### C# - Member name = enclosing type\\n\\nI encountered this issue first for `block.block`. In C# member names are not allowed to be the same as the enclosing type.\\n\\nAs i am not a very experienced C# developer, i am still rather undecided on how to handle this issue. \\nSo far I have been using `Block.block`, as I am thinking about using PascalCase for all classes in C#. And if only to mute the warnings in VS Community.\\n\\nIf you have a strong opinion about this topic, please let me know! I am interested in hearing your thoughts.\\n\\n#### Transpiled names\\n\\nAnd back to `classDiagram.``class`` `. While JavaScript does not seem to care about this topic to much, Python does.\\n\\nJavaScript gives us the best result, the F# backtick syntax is transpiled to a simple camelCase name `classDiagram.class`.\\n\\nPython on the other hand has `class` also as reserved keyword. Fable transpiles it to `classDiagram.class_`. \\nWhich raises the question if i should simply apply this syntax to all cases with naming problems.\\n\\n### Docs/Native Test Maintainance\\n\\nThe core library + C# wrapper were done rather quickly. I can also recycle my F# unit tests to check if transpilation works as expected, using [Fable.Pyxpecto](https://github.com/Freymaurer/Fable.Pyxpecto).\\n\\nBut testing correct native accessibillity and writing docs (showcasing the test cases) was the most time consuming part and something i am not happy with.\\n\\nHere are some ideas on how to improve this:\\n\\n- Theoretically, i could use the transpiled tests for docs. But i still have to remove the Fable specific helper functions and replace them with native ones.\\n- *[Kevin S.][KevinS]* had an idea, repurposing jupyter notebooks for docs and testing. To at least unify the testing and docs. \\n\\nIf you have any ideas on how to improve this, please let me know!\\n\\n\\n{/* References */}\\n[Fable]: https://fable.io\\n[REPL]: https://fable.io/repl/\\n[KevinS]: https://github.com/kMutagene"}]}')}}]); \ No newline at end of file diff --git a/assets/js/831440af.66ef3cd6.js b/assets/js/831440af.66ef3cd6.js deleted file mode 100644 index 01ce2ae..0000000 --- a/assets/js/831440af.66ef3cd6.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[7466],{4463:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>h,contentTitle:()=>o,default:()=>m,frontMatter:()=>l,metadata:()=>c,toc:()=>d});var s=t(4848),a=t(8453),i=t(1470),r=t(9365);const l={slug:"Library Design",title:"Library Design",authors:"freyk",tags:["fable","library","api","design","helper"]},o="Libary Design",c={permalink:"/Siren/blog/Library Design",editUrl:"https://github.com/Freymaurer/Siren/tree/main/docs/blog/2024-05-06-fable-library-design.mdx",source:"@site/blog/2024-05-06-fable-library-design.mdx",title:"Library Design",description:"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#.",date:"2024-05-06T00:00:00.000Z",tags:[{label:"fable",permalink:"/Siren/blog/tags/fable"},{label:"library",permalink:"/Siren/blog/tags/library"},{label:"api",permalink:"/Siren/blog/tags/api"},{label:"design",permalink:"/Siren/blog/tags/design"},{label:"helper",permalink:"/Siren/blog/tags/helper"}],readingTime:23.445,hasTruncateMarker:!1,authors:[{name:"Kevin Frey",title:"Maintainer of Siren",url:"https://github.com/Freymaurer",imageURL:"https://github.com/Freymaurer.png",key:"freyk"}],frontMatter:{slug:"Library Design",title:"Library Design",authors:"freyk",tags:["fable","library","api","design","helper"]},unlisted:!1},h={authorsImageUrls:[void 0]},d=[{value:"Idea \ud83d\udca1",id:"idea-",level:2},{value:"What is Fable?",id:"what-is-fable",level:2},{value:"Benefits",id:"benefits",level:3},{value:"API Design",id:"api-design",level:2},{value:"Using [<AttachMembers>]",id:"using-attachmembers",level:3},{value:"C# Compatibility",id:"c-compatibility",level:3},{value:"Code generator helper",id:"code-generator-helper",level:4},{value:"Maintainability",id:"maintainability",level:4},{value:"Python/JavaScript import and Index files",id:"pythonjavascript-import-and-index-files",level:3},{value:"Publish",id:"publish",level:2},{value:".NET",id:"net",level:3},{value:"Pypi",id:"pypi",level:3},{value:"JavaScript (+Types)",id:"javascript-types",level:3},{value:"Deep Dive",id:"deep-dive",level:2},{value:"Overloads",id:"overloads",level:3},{value:"Erased Unions",id:"erased-unions",level:4},{value:"JavaScript optional parameters",id:"javascript-optional-parameters",level:3},{value:"Member Names",id:"member-names",level:3},{value:"F# reserved keywords",id:"f-reserved-keywords",level:4},{value:"C# - Member name = enclosing type",id:"c---member-name--enclosing-type",level:4},{value:"Transpiled names",id:"transpiled-names",level:4},{value:"Docs/Native Test Maintainance",id:"docsnative-test-maintainance",level:3}];function p(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",input:"input",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#."}),"\n",(0,s.jsx)(n.admonition,{title:"Fable.Multiverse",type:"tip",children:(0,s.jsxs)(n.p,{children:["To make it as easy as possible to create a Fable library that publishes in multiple languages, I created a template called ",(0,s.jsx)(n.a,{href:"https://github.com/Freymaurer/Fable.Multiverse",children:"Fable.Multiverse"})," ","\ud83c\udf89"]})}),"\n",(0,s.jsx)(n.h2,{id:"idea-",children:"Idea \ud83d\udca1"}),"\n",(0,s.jsxs)(n.p,{children:["During a hackathon for our research data management consortium, we were discussing ideas for visualizing graph like structures in a way that allows easy gitlab integration and can be understood and used by any level of user. The idea we pursued was to add ",(0,s.jsx)(n.code,{children:".md"})," files with mermaid graphs for an easy overview. So why not write a domain specific language for mermaid graphs to make creation of such graphs easier and more error proof. Good idea, but what programming language should we use? In our consortium we have several groups, some using JavaScript, some Python, some (us) .NET or more specifically F#. Because i already have quite some experience using ",(0,s.jsx)(n.a,{href:"https://fable.io",children:"Fable"}),", i did the mental checklist to see if it would be a good fit for this project."]}),"\n",(0,s.jsxs)(n.ul,{className:"contains-task-list",children:["\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Does not need any dependencies."})," Mermaid graphs, are mostly YAML, so no complex syntax."]}),"\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Does not require IO interaction."})," We can simply focus on writing our mermaid graph as string and allow the user to do whatever they want with it."]}),"\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Only Fable compatible languages needed."})," We are already very happy offering such a tool in Python, JavaScript and F#."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:".. and thats it \ud83c\udf89 So we can start developing a libary with one codebase for 4 [5] languages."}),"\n",(0,s.jsx)(n.h2,{id:"what-is-fable",children:"What is Fable?"}),"\n",(0,s.jsxs)(n.p,{children:["Fable is a F# to X transpiler. It started out targeting only JavaScript, using a naming reference to the popular ",(0,s.jsx)(n.a,{href:"https://babeljs.io",children:"Babel"})," JavaScript transpiler. Now Fable aims to support multiple languages, all in different states. At the time of writing, the offical Fable docs state the following:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Language"}),(0,s.jsx)(n.th,{children:"Status"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JavaScript"}),(0,s.jsx)(n.td,{children:"Stable"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"TypeScript"}),(0,s.jsx)(n.td,{children:"Stable"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Dart"}),(0,s.jsx)(n.td,{children:"Beta"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Python"}),(0,s.jsx)(n.td,{children:"Beta"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Rust"}),(0,s.jsx)(n.td,{children:"Alpha"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"PHP"}),(0,s.jsx)(n.td,{children:"Experimental"})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"benefits",children:"Benefits"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Type Safety"}),". F# is a statically typed language, which means that the compiler can catch many errors before they even happen."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Lightweight Syntax"}),". F# has a very lightweight syntax, which makes it easy to read and write. It does not require a lot of boilerplate code and you can get right into the meat of your program."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Testing(?)"}),". Main codebase is written in F# as well as most tests. This allows us to also transpile the tests to other languages and run them there. This is a big advantage, as we can be sure that the tests are the same in all languages."]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{title:"But why the question mark behind testing?",type:"note",children:(0,s.jsx)(n.p,{children:"Because we can recycle the tests, to ensure correct functionality, but we still must test if the library can be used from all supported languages without hurdles."})}),"\n",(0,s.jsx)(n.h2,{id:"api-design",children:"API Design"}),"\n",(0,s.jsx)(n.p,{children:"To make the code look and feel as native as possible in all languages, there are some things we need to consider. But first let us have a look at fable transpiled code."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["The following code will use the ",(0,s.jsx)(n.a,{href:"https://fable.io/repl/",children:"Fable REPL"})," to transpile code for easy showcasing!"]})}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'let helloWorld = printfn "Hello World"\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { printf, toConsole } from "fable-library-js/String.js";\n\nexport const helloWorld = toConsole(printf("Hello World"));\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'from fable_library_js.string import (to_console, printf)\n\nhello_world: None = to_console(printf("Hello World"))\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"We can already notice some things:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Fable tries to transpile into native syntax, so for example snake_case in Python and camelCase in JavaScript"}),"\n",(0,s.jsxs)(n.li,{children:["Fable has some wrappers for functions which might have native equivalents. In F# ",(0,s.jsx)(n.code,{children:"printfn"})," is used to print to the console, in JavaScript ",(0,s.jsx)(n.code,{children:"console.log"})," and in Python ",(0,s.jsx)(n.code,{children:"print"}),". But Fable uses their own printf function to ensure 100% correct f# transpilation."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Next we will have a look at a class with some member functions."}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"type MyClass =\n static member add (x: int) (y: int) = x + y\n"})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n}\n\nexport function MyClass_$reflection() {\n return class_type("Test.MyClass", undefined, MyClass);\n}\n\nexport function MyClass_add(x, y) {\n return x + y;\n}\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'from fable_library_js.reflection import (TypeInfo, class_type)\n\ndef _expr0() -> TypeInfo:\n return class_type("Test.MyClass", None, MyClass)\n\n\nclass MyClass:\n ...\n\nMyClass_reflection = _expr0\n\ndef MyClass_add(x: int, y: int) -> int:\n return x + y\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Oh no, this does not look good. Fable does a thing called ",(0,s.jsx)(n.a,{href:"https://fable.io/docs/javascript/features.html#name-mangling",children:(0,s.jsx)(n.em,{children:"name mangling"})}),".\nHave a look at the offical docs for a deeper view on this topic.\nFor now its enought to know, this is done to allow overloading functions in F#."]}),"\n",(0,s.jsxs)(n.h3,{id:"using-attachmembers",children:["Using ",(0,s.jsx)(n.code,{children:"[]"})]}),"\n",(0,s.jsx)(n.p,{children:"But we can tell Fable that we know what we are doing and ignore name mangling."}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"open Fable.Core\n\n[]\ntype MyClass =\n static member add (x: int) (y: int) = x + y\n"})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static add(x, y) {\n return x + y;\n }\n}\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:"from fable_library_js.reflection import (TypeInfo, class_type)\n\nclass MyClass:\n @staticmethod\n def add(x: int, y: int) -> int:\n return x + y\n"})})})]}),"\n",(0,s.jsxs)(n.p,{children:["That looks better and allows us to do the following in all 3 languages: ",(0,s.jsx)(n.code,{children:"MyClass.add"}),".\nThis is the basic design i chose to use for most user facing api. All F#/Fable code not easily usable from other languages is hidden behind a facade like this."]}),"\n",(0,s.jsx)(n.h3,{id:"c-compatibility",children:"C# Compatibility"}),"\n",(0,s.jsxs)(n.p,{children:["Strangely enough allowing C# users the same ease of use as Python and JavaScript users is the hardest.\nThis is because C# has some issues with F# ",(0,s.jsx)(n.em,{children:"optional parameters"})," and F# ",(0,s.jsx)(n.em,{children:"tuples"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"In F# we can define a function like this:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'[]\ntype flowchart =\n static member raw (txt: string) = FlowchartElement txt\n static member id (txt: string) = FlowchartElement txt\n static member node (id: string, ?name: string) : FlowchartElement = ...\n\nflowchart.node("My id")\nflowchart.node("My id", "My name")\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Using the F# function in C# will result in an error, when you try to do ",(0,s.jsx)(n.code,{children:'flowchart.node("My id")'}),", as ",(0,s.jsx)(n.code,{children:"?name"})," is a ",(0,s.jsx)(n.code,{children:"Microsoft.FSharp.Core.FSharpOption"})," without any default information."]}),"\n",(0,s.jsx)(n.p,{children:"By creating a C# access layer we can avoid this issue for C# users:"}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["The C# extensions for optional parameters and tuples are taken from ",(0,s.jsx)(n.a,{href:"https://github.com/plotly/Plotly.NET",children:"Plotly.NET"})," with the help from my dear colleague ",(0,s.jsx)(n.a,{href:"https://github.com/kMutagene",children:"Kevin Schneider"}),"."]})}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"flow",label:"Flowchart.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public static class flowchart\n{\n public static FlowchartElement raw(string txt) => Siren.flowchart.raw(txt);\n\n public static FlowchartElement id(string txt) => Siren.flowchart.raw(txt);\n\n public static FlowchartElement node(string id, Optional name = default) =>\n Siren.flowchart.node(id, name.ToOption());\n //...\n}\n"})})}),(0,s.jsx)(r.A,{value:"fsharp",label:"OptionExtension.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'/// \n/// Helper type for handling the special way the Plotly.NET core API uses generics.\n/// In short, the problem arises because many optional parameters of Plotly.NET\'s core API are generics \n/// with a type constraint for `IConvertible`. This means that these parameters can be both value and reference types \n/// (e.g. `double` and `System.DateTime` both implement IConvertible).\n/// If we now have a optional parameter of type `T? where T: IConvertible` the compiler will not allow this \n/// without further type constrainst to eithe reference or value type.\n/// This is a problem because we want to 1. allow both, and 2. have a reliable way of determining if the value was not set \n/// because the F# API expects to be passed `Option.None` in that case.\n/// There exist other workarounds like checking if the value is default or null, but that changes valid default values actually set to null as well.\n/// \n/// \n/// The value to mark as optional\n/// Wether or not the wrapped value is valid. This is used downstream to determine wether to wrap this value into `Option.Some` (if true) or `Option.None` (if false)\npublic readonly record struct Optional(T Value, bool IsSome)\n{\n /// \n /// \n /// \n /// \n public static implicit operator Optional(T Value) => new(Value, true);\n\n}\n/// \n/// Extension methods for the `Optional` class\n/// \npublic static class OptionalExtensions\n{\n /// \n /// Converts the `Optional` value to `Some(value)` if the value is valid, or `None` if it is not.\n /// \n /// \n /// The `Optional` value to convert to a F# Option\n /// opt converted to `Option`\n static internal Microsoft.FSharp.Core.FSharpOption ToOption(this Optional opt) => opt.IsSome ? new(opt.Value) : Microsoft.FSharp.Core.FSharpOption.None;\n}\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"A similar issue arises with F# tuples:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"flow",label:"Flowchart.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"[]\ntype flowchart =\n static member stylesNode (nodeId: string, styles:#seq) = Flowchart.formatNodeStyles [nodeId] (List.ofSeq styles) |> FlowchartElement\n"})})}),(0,s.jsx)(r.A,{value:"extension",label:"TupleExtension.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"/// \n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\n/// \ninternal static class TupleExtensions\n{\n /// \n /// Converts a tuple.\n /// \n internal static Tuple ToTuple(this ValueTuple t) => Tuple.Create(t.Item1, t.Item2);\n}\n"})})}),(0,s.jsx)(r.A,{value:"fsharp",label:"Flowchart.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"/// \n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\n/// \npublic static class flowchart\n{\n public static FlowchartElement stylesNode(string nodeId, (string, string)[] styles) =>\n Siren.flowchart.stylesNode(nodeId, styles.Select(t => t.ToTuple()));\n}\n"})})})]}),"\n",(0,s.jsx)(n.h4,{id:"code-generator-helper",children:"Code generator helper"}),"\n",(0,s.jsx)(n.p,{children:"These incompatibilities are not only annoying but providing a consistently native C# experience, requires a wrapping for all apis.\nTo make this easier, i created a code generator that takes a F# file and generates the C# wrapper for it. .. Or at least 95% of it. The rest is done by hand."}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Code Generator"}),(0,s.jsx)("div",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open System.Reflection\n\n[]\nlet FSharpOptionDefault = "Optional"\n\nlet transformParameterTypeName (paramTypeName: string)=\n match paramTypeName with\n | "String" -> "string"\n | "Int32" -> "int"\n | "Double" -> "double"\n | "FSharpOption`1" -> FSharpOptionDefault // this is not always true but a good approximation\n | "Tuple`2" -> "(string,string)" // this is not always true but a good approximation\n | "Boolean" -> "bool"\n | _ -> paramTypeName\n\ntype ParameterInfo = {\n Type: string\n Name: string\n} with\n member this.FSharpParam =\n match this with\n | {Type = FSharpOptionDefault} -> this.Name + ".ToOption()"\n | _ -> this.Name\n member this.CSharpParam =\n match this with\n | {Type = FSharpOptionDefault} -> sprintf "%s %s = default" this.Type this.Name\n | _ -> sprintf "%s %s" this.Type this.Name\n\n static member create(typeName: string, name: string) =\n {Type = transformParameterTypeName typeName; Name = name}\n\nlet generateCSharpCode<\'A>() =\n\n let t = typeof<\'A>\n let members = t.GetMethods(BindingFlags.Static ||| BindingFlags.Public)\n\n let mutable csharpCode = sprintf "public static class %s\\n{\\n" t.Name\n for m in members do\n let methodName = \n let name0 = m.Name\n if name0.StartsWith("get_") then\n name0.Substring(4)\n else\n name0\n let returnType = m.ReturnType.Name\n let params0 = \n m.GetParameters() \n |> Array.map (fun p -> ParameterInfo.create(p.ParameterType.Name, p.Name))\n let csharpParameters =\n if params0.Length = 0 then\n ""\n else \n params0\n |> Array.map _.CSharpParam\n |> String.concat(", ")\n |> fun s -> "(" + s + ")"\n \n let fsharpParameters = \n if params0.Length = 0 then\n ""\n else \n params0 \n |> Array.map _.FSharpParam\n |> String.concat(", ")\n |> fun s -> "(" + s + ")"\n \n let methodSignature = $"public static {transformParameterTypeName returnType} {methodName}{csharpParameters}"\n let methodBody = \n if methodName.StartsWith("get_") then\n let withoutGet = methodName.Substring(4)\n $"return Siren.{t.Name}.{withoutGet};"\n else\n $" => Siren.{t.Name}.{methodName}{fsharpParameters};"\n csharpCode <- csharpCode + $" {methodSignature}\\n {methodBody}\\n"\n\n csharpCode <- csharpCode + "}\\n"\n csharpCode\n\nlet test() = \n generateCSharpCode() // Here you can pass any type you want to generate C# code for\n |> printfn "%A"\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"This is something i did not want to spend a lot of time on, so when i quickly wrote this and noticed it was able to create most of the C# code correctly.\nI think improving this code to create everything perfectly would be awesome, but would have taken me longer thant fixing the few mistakes it makes by hand."}),"\n",(0,s.jsx)(n.h4,{id:"maintainability",children:"Maintainability"}),"\n",(0,s.jsx)(n.p,{children:"This is a big issue. Whenever i update my f# api i must also update the c# wrapper.\nChanges are mostly catched by the compiler but missings functions are not."}),"\n",(0,s.jsx)(n.p,{children:"That is why i added unit tests to check the count and name of a c# and f# class and compare it for equality:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public static class Utils\n{\n public static int GetMemberCount(Type type)\n {\n var members = type.GetMembers();\n return members.Length;\n }\n\n public static List GetMemberNameDifferences(Type type1, Type type2)\n {\n List differences = new List();\n //transform string to lower\n var type1Members = type1.GetMembers().Select(m => m.Name.ToLower());\n var type2Members = type2.GetMembers().Select(m => m.Name.ToLower());\n differences.AddRange(type1Members.Except(type2Members));\n differences.AddRange(type2Members.Except(type1Members));\n\n return differences;\n }\n\n public static void CompareClasses(Type csharpType, Type fsharpType)\n {\n int csharpMemberCount = GetMemberCount(csharpType);\n int fsharpMemberCount = GetMemberCount(fsharpType);\n List differences = GetMemberNameDifferences(fsharpType, csharpType);\n\n Assert.Empty(differences);\n Assert.Equal(fsharpMemberCount, csharpMemberCount);\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"This at least helps me to catch missing functions in the c# wrapper, even if i still have to write them by hand."}),"\n",(0,s.jsx)(n.h3,{id:"pythonjavascript-import-and-index-files",children:"Python/JavaScript import and Index files"}),"\n",(0,s.jsxs)(n.p,{children:["F# and C# use namespaces to organize code. I can have multiple files with the same namespace and access all functions simply by writing ",(0,s.jsx)(n.code,{children:"open Siren"}),"/",(0,s.jsx)(n.code,{children:"using Siren.Sea;"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"In Python and JavaScript this is not possible. Here imports happen on a file basis. So i needed to have a single file that imports all other files and exports them."}),"\n",(0,s.jsx)(n.p,{children:"Luckily this file can be created automatically!"}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Index File"}),(0,s.jsx)("div",{children:(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"util",label:"Util.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.Util\n\nopen System\nopen System.IO\nopen System.Text.RegularExpressions\n\ntype FileInformation = {\n FilePath : string\n Lines : string []\n} with\n static member create(filePath: string, lines: string []) = {\n FilePath = filePath\n Lines = lines\n }\n\nlet getAllFiles(path: string, extension: string) = \n let options = EnumerationOptions()\n options.RecurseSubdirectories <- true\n IO.Directory.EnumerateFiles(path,extension,options)\n |> Seq.filter (fun s -> s.Contains("fable_modules") |> not)\n |> Array.ofSeq\n\nlet findClasses (rootPath: string) (cois: string []) (regexPattern: string -> string) (filePaths: seq) = \n let files = [|\n for fp in filePaths do\n yield FileInformation.create(fp, System.IO.File.ReadAllLines (fp))\n |]\n let importStatements = ResizeArray()\n let findClass (className: string) = \n /// maybe set this as default if you do not want any whitelist\n let classNameDefault = @"[a-zA-Z_0-9]"\n let pattern = Regex.Escape(className) |> regexPattern\n let regex = Regex(pattern)\n let mutable found = false\n let mutable result = None\n let enum = files.GetEnumerator()\n while not found && enum.MoveNext() do\n let fileInfo = enum.Current :?> FileInformation\n for line in fileInfo.Lines do\n let m = regex.Match(line)\n match m.Success with\n | true -> \n found <- true\n result <- Some <| (className, IO.Path.GetRelativePath(rootPath,fileInfo.FilePath))\n | false ->\n ()\n match result with\n | None ->\n failwithf "Unable to find %s" className\n | Some r ->\n importStatements.Add r\n for coi in cois do findClass coi\n importStatements\n |> Array.ofSeq\n\nlet writeIndexFile (path: string) (fileName: string) (content: string) =\n let filePath = Path.Combine(path, fileName)\n File.WriteAllText(filePath, content)\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"Indexjs.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.JS\n\nlet private getAllJsFiles (path: string) fileExtension = \n Util.getAllFiles(path,$"*.{fileExtension}")\n\nlet private pattern (className: string) = sprintf @"^export class (?%s)+[\\s{].*({)?" className\n\nlet private findJsClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \n Util.findClasses rootPath whiteList pattern filePaths\n\nopen System.Text\n\nlet private createImportStatements (info: (string*string) []) =\n let sb = StringBuilder()\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\n for filePath, imports in importCollection do\n let p = filePath.Replace("\\\\","/").Replace("ts","js")\n sb.Append "export { " |> ignore\n sb.AppendJoin(", ", imports) |> ignore\n sb.Append " } from " |> ignore\n sb.Append (sprintf "\\"./%s\\"" p) |> ignore\n sb.Append ";" |> ignore\n sb.AppendLine() |> ignore\n sb.ToString()\n\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string [], fileExtension: string) =\n getAllJsFiles rootPath fileExtension\n |> findJsClasses rootPath whiteList\n |> createImportStatements\n |> Util.writeIndexFile rootPath fileName\n\nlet generate(rootPath: string) (ts: bool) = \n let extension = if ts then "ts" else "js"\n generateIndexfile(rootPath, $"index.{extension}", WhiteList.WhiteList, extension)\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Indexpy.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'let private getAllJsFiles(path: string) = \n Util.getAllFiles(path,"*.py")\n\nlet private pattern (className: string) = sprintf @"^class (?%s)+(\\(|:).*$" className\n\nlet private findPyClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \n Util.findClasses rootPath whiteList pattern filePaths\n\nopen System.Text\n\nlet private createImportStatements (info: (string*string) []) =\n let sb = StringBuilder()\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\n for filePath, imports in importCollection do\n let p = filePath |> Path.GetFileNameWithoutExtension\n sb.Append (sprintf "from .%s import " p) |> ignore\n sb.AppendJoin(", ", imports) |> ignore\n sb.AppendLine() |> ignore\n sb.ToString()\n\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string []) =\n getAllJsFiles(rootPath)\n |> findPyClasses rootPath whiteList\n |> createImportStatements\n |> Util.writeIndexFile rootPath fileName\n\nlet generate(rootPath: string) (name: string) = \n // code to make camelcase to snakecase\n /// This is because we currently snake_case everything that does not start with a capital letter\n let camelCaseToSnakeCase (str: string) = \n if Char.IsUpper str.[0] then\n str \n else\n str \n |> Seq.fold (fun (acc: string) c -> \n if Char.IsUpper c then \n acc + "_" + string (Char.ToLower c) \n else \n acc + string c\n ) ""\n let snake_case_white_list = WhiteList.WhiteList |> Array.map camelCaseToSnakeCase\n generateIndexfile(rootPath, name, snake_case_white_list)\n'})})}),(0,s.jsx)(r.A,{value:"whitelist",label:"WhiteList.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.WhiteList\n\nlet WhiteList = [|\n "SirenElement"\n // ThemeVaruiables\n "themeVariable"\n "quadrantTheme"; \n "gitTheme"; \n "timelineTheme"; \n "xyChartTheme"; \n "pieTheme";\n // Config\n "graphConfig";\n "flowchartConfig"; \n "sequenceConfig"; \n "ganttConfig"; \n "journeyConfig"; \n "timelineConfig"; \n "classConfig"; \n "stateConfig"; \n "erConfig"; \n "quadrantChartConfig"; \n "pieConfig"; \n "sankeyConfig"; \n "xyChartConfig"; \n "mindmapConfig"; \n "gitGraphConfig"; \n "requirementConfig"; \n // Graphs and Helpers\n "formatting"; \n "direction"; \n "flowchart"; \n "notePosition"; \n "sequence"; \n "classMemberVisibility"; \n "classMemberClassifier"; \n "classDirection"; \n "classCardinality"; \n "classRltsType"; \n "classDiagram"; \n "stateDiagram"; \n "erKey"; \n "erCardinality"; \n "erDiagram"; \n "journey"; \n "ganttTime"; \n "ganttTags"; \n "ganttUnit"; \n "gantt"; \n "pieChart"; \n "quadrant"; \n "rqRisk"; \n "rqMethod"; \n "requirement"; \n "gitType"; \n "git"; \n "mindmap"; \n "timeline"; \n "sankey"; \n "xyChart"; \n "block"; \n "theme"; \n "siren"; \n|]\n'})})})]})})]}),"\n",(0,s.jsx)(n.p,{children:"The resulting files look like this:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"js",label:"index.js",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'export { SirenElement } from "./SirenTypes.js";\nexport { themeVariable, quadrantTheme, gitTheme, timelineTheme, xyChartTheme, pieTheme } from "./ThemeVariables.js";\nexport { graphConfig, flowchartConfig, sequenceConfig, ganttConfig, journeyConfig, timelineConfig, classConfig, stateConfig, erConfig, quadrantChartConfig, pieConfig, sankeyConfig, xyChartConfig, mindmapConfig, gitGraphConfig, requirementConfig } from "./Config.js";\nexport { formatting, direction, flowchart, notePosition, sequence, classMemberVisibility, classMemberClassifier, classDirection, classCardinality, classRltsType, classDiagram, stateDiagram, erKey, erCardinality, erDiagram, journey, ganttTime, ganttTags, ganttUnit, gantt, pieChart, quadrant, rqRisk, rqMethod, requirement, gitType, git, mindmap, timeline, sankey, xyChart, block, theme, siren } from "./Siren.js";\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"index.py/__init__.py",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-py",children:"from .siren_types import SirenElement\nfrom .theme_variables import theme_variable, quadrant_theme, git_theme, timeline_theme, xy_chart_theme, pie_theme\nfrom .config import graph_config, flowchart_config, sequence_config, gantt_config, journey_config, timeline_config, class_config, state_config, er_config, quadrant_chart_config, pie_config, sankey_config, xy_chart_config, mindmap_config, git_graph_config, requirement_config\nfrom .siren import formatting, direction, flowchart, note_position, sequence, class_member_visibility, class_member_classifier, class_direction, class_cardinality, class_rlts_type, class_diagram, state_diagram, er_key, er_cardinality, er_diagram, journey, gantt_time, gantt_tags, gantt_unit, gantt, pie_chart, quadrant, rq_risk, rq_method, requirement, git_type, git, mindmap, timeline, sankey, xy_chart, block, theme, siren\n"})})})]}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["Python packages will default import whatever is in the ",(0,s.jsx)(n.code,{children:"__init__.py"})," file. So we must simply name the index file for python as such (at least for publishing)."]})}),"\n",(0,s.jsx)(n.h2,{id:"publish",children:"Publish"}),"\n",(0,s.jsx)(n.p,{children:"As soon as we get to the publishing step everything is back to standard for the respective languages."}),"\n",(0,s.jsx)(n.h3,{id:"net",children:".NET"}),"\n",(0,s.jsxs)(n.p,{children:["The easiest. We already have the required project files, no need to transpile. So we can simply use ",(0,s.jsx)(n.code,{children:"dotnet pack"})," to create .nupkg files.\nThen ",(0,s.jsx)(n.code,{children:"dotnet nuget push"})," to push to ",(0,s.jsx)(n.a,{href:"https://www.nuget.org",children:"nuget.org"}),"."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h3,{id:"pypi",children:"Pypi"}),"\n",(0,s.jsxs)(n.p,{children:["Also quite easy. Transpile f# code to python code, copy ",(0,s.jsx)(n.code,{children:"pyproject.toml"})," and ",(0,s.jsx)(n.code,{children:"README.md"})," into the ",(0,s.jsx)(n.code,{children:"dist"})," folder and create ",(0,s.jsx)(n.code,{children:"index.py"})," file.\nRun ",(0,s.jsx)(n.code,{children:"python -m poetry build"})," to create a publishing files. Then publish files with ",(0,s.jsx)(n.code,{children:"python -m poetry publish"})]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h3,{id:"javascript-types",children:"JavaScript (+Types)"}),"\n",(0,s.jsxs)(n.p,{children:["For this i followed the Example of ",(0,s.jsx)(n.a,{href:"https://fable.io/blog/2023/2023-04-20-Better_Typed_than_Sorry.html",children:"Better Typed than Sorry"})," by ",(0,s.jsx)(n.a,{href:"https://github.com/alfonsogarciacaro",children:"Alfonso Garc\xeda-Caro"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Transpile F# to TypeScript, then use ",(0,s.jsx)(n.code,{children:"tsc"})," to transpile TypeScript to JavaScript and type information files (",(0,s.jsx)(n.code,{children:"*.d.ts"}),"). Add the ",(0,s.jsx)(n.code,{children:"index.js"})," file to the ",(0,s.jsx)(n.code,{children:"dist folder"}),"\nThen publish to npm with ",(0,s.jsx)(n.code,{children:"npm publish"}),"."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h2,{id:"deep-dive",children:"Deep Dive"}),"\n",(0,s.jsx)(n.p,{children:"From here on are some additional issues i encountered during development."}),"\n",(0,s.jsx)(n.h3,{id:"overloads",children:"Overloads"}),"\n",(0,s.jsxs)(n.p,{children:["The concept of allowing different inputs for the same function exists in f#, as well as python and javascript.\nBy using the ",(0,s.jsx)(n.code,{children:"[]"})," attribute we are not longer allowed to use standard f# overloads.\nBecause JavaScript does not have the same kind of type interference it is unable to recognice which function should be invoked:"]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\n\n[]\ntype MyClass =\n static member add (x: int, y: int) = x * y // this should be invoked\n static member add (x: string, y: string) = x + y\n\nlet result = MyClass.add(10, 20)\n\nprintfn "%A" result\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\nimport { printf, toConsole } from "fable-library-js/String.js";\n\nexport class MyClass {\n constructor() {\n }\n static add(x, y) {\n return x * y;\n }\n static add(x, y) { // This shadows the function above and is invoked\n return x + y;\n }\n}\n\nexport const result = MyClass.add(10, 20); // This will return 30\n\ntoConsole(printf("%A"))(result);\n\n'})})})]}),"\n",(0,s.jsx)(n.h4,{id:"erased-unions",children:"Erased Unions"}),"\n",(0,s.jsxs)(n.p,{children:["It is possible to imitate js overload behaviour by using ",(0,s.jsx)(n.a,{href:"https://fable.io/docs/javascript/features.html#erased-unions",children:"erased unions"}),".\nHere we use a Fable provides discriminate union called ",(0,s.jsx)(n.code,{children:"U2"}),"(",(0,s.jsx)(n.code,{children:"U3"}),", ",(0,s.jsx)(n.code,{children:"U4"}),"...). After transpilation it is replaced by a js type check."]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\nopen Fable.Core.JsInterop\n\n[]\ntype MyClass =\n static member test (arg: U2) = \n match arg with\n | U2.Case1 s -> printfn "This is a string: %s" s\n | U2.Case2 i -> printfn "This is a integer: %i" i\n\nlet result = MyClass.test(U2.Case2 10) // or MyClass.test(!^10)\n\nprintfn "%A" result\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { printf, toConsole } from "fable-library-js/String.js";\nimport { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static test(arg) {\n if (typeof arg === "number") {\n const i = arg;\n toConsole(printf("This is a integer: %i"))(i);\n }\n else {\n const s = arg;\n toConsole(printf("This is a string: %s"))(s);\n }\n }\n}\n\nexport const result = MyClass.test(10);\n\ntoConsole(printf("%A"))(); // This is a integer: 10\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"As you can see, this looks really nice in JavaScript, but is cumbersome to use in F#."}),"\n",(0,s.jsx)(n.p,{children:"It would be possible to do do both and use f# overloads and shadow them with the erased union. But this would add more additional maintainance work."}),"\n",(0,s.jsxs)(n.admonition,{title:"Compiler Statements",type:"info",children:[(0,s.jsxs)(n.p,{children:["We can use ",(0,s.jsx)(n.code,{children:"#if FABLE_COMPILER ... #else ... #endif"})," syntax to include code only in certain compiler states."]}),(0,s.jsx)(n.p,{children:"In the following example the erased union is only used (and accessible!) when the code is transpiled by Fable to JavaScript."})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'[]\ntype MyClass =\n// highlight-next-line\n#if FABLE_COMPILER_JAVASCRIPT\n static member test (arg: U2) = \n match arg with\n | U2.Case1 s -> printfn "This is a string: %s" s\n | U2.Case2 i -> printfn "This is a integer: %i" i\n// highlight-next-line\n#else\n static member test(arg: int) =\n printfn "This is a integer: %i" arg\n static member test(arg: string) =\n printfn "This is a string: %s" arg\n// highlight-next-line\n#endif\n\nlet result = MyClass.test(10)\n'})}),"\n",(0,s.jsx)(n.p,{children:"This would allow us to use the erased union only in JavaScript and use the F# overloads in F#. But i have not investigated how this would work for python \ud83d\ude05."}),"\n",(0,s.jsx)(n.p,{children:"Due to the additional workload i decided to avoid using overloads in the api. Instead i tried finding the core functions and functions, which allow additional inputs with a different name:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"type sequence =\n static member note(id: string, text: string, ?notePosition: NotePosition) = //..\n static member noteSpanning(id1: string, id2, text: string, ?notePosition: NotePosition) = //..\n"})}),"\n",(0,s.jsx)(n.h3,{id:"javascript-optional-parameters",children:"JavaScript optional parameters"}),"\n",(0,s.jsx)(n.p,{children:"Using functions with multiple optional parameters is easily done in F#, C# and Python, but can get quite annoying in JavaScript:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'// tripple null ...\nrequirement.requirement("my id", null, null, null, rqMethod.test)\n'})})}),(0,s.jsx)(r.A,{value:"f",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'// easy!\nrequirement.requirement("My Id", rqMethod = rqMethod.analysis)\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'# easy!\nrequirement.requirement("My Id", rq_method = rq_method.analysis)\n'})})}),(0,s.jsx)(r.A,{value:"c",label:"C#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// easy!\nRequirement.requirement("My Id", rqMethod: rqMethod.analysis)\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["The JavaScript native approach would be using an object with only the values you want to set.\nThere is even a way to tell Fable to transpile parameters as object using the ",(0,s.jsx)(n.code,{children:"[ParamObject]"})," attribute."]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"f",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\nopen Fable.Core.JsInterop\n\n[]\ntype MyClass =\n [] // Start creating obj from params at index 1\n static member test (name: int, ?id: string, ?text: string, ?rqRisk: string, ?rqMethod: string) =\n 0\n\n\nMyClass.test(10, rqRisk = "Hello")\nMyClass.test(10)\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static test(name, { id, text, rqRisk, rqMethod }) {\n return 0;\n }\n}\n\nMyClass.test(10, {\n rqRisk: "Hello",\n});\n\nMyClass.test(10, {}); // Oh oh. Why an empty object?\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["As you can see adding ",(0,s.jsx)(n.em,{children:"no"})," optional parameters requires an empty object, as Fable checks if the value at object key xyz is null and not if the object is null.\nWithout the empty object the function would throw an error, whenever any of the optional parameters is referenced in the function. (Bad example as i just return 0)."]}),"\n",(0,s.jsx)(n.h3,{id:"member-names",children:"Member Names"}),"\n",(0,s.jsx)(n.p,{children:"Different languages have different expectations for member names. Aside from styling best practises, there are some things that are not possible in all languages."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsx)(n.p,{children:"F# typically uses PascalCase for class names and camelCase for member names.\nFor easier usage i ignore this rule and used camelCase for everything."})}),"\n",(0,s.jsx)(n.h4,{id:"f-reserved-keywords",children:"F# reserved keywords"}),"\n",(0,s.jsx)(n.p,{children:"The first issue i encountered where reserved keywords in F#."}),"\n",(0,s.jsxs)(n.p,{children:["For example ",(0,s.jsx)(n.code,{children:"classDiagram.``class``"}),". ",(0,s.jsx)(n.em,{children:'"class"'})," is a reserved keyword which is not allowed in F#.\nThe standard solution is wrapping the name in backticks. But at least for me on VisualStudio Community this resulted in issues with my auto complete.\nThis resulted in me handling this issue inconsistently. The issues i encountered were mostly in (optional) parameters, which is why i changed their names to PascalCase:"]}),"\n",(0,s.jsx)(n.p,{children:"For members i mostly stayed true to the backtick syntax."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"[]\ntype classMemberClassifier =\n // abstract is a reserved keyword\n static member Abstract = ClassMemberClassifier.Abstract\n // static is a reserved keyword\n static member Static = ClassMemberClassifier.Static\n static member custom str = ClassMemberClassifier.Custom str\n\n[]\ntype classDiagram =\n static member ``class`` (id: string, members: #seq) = \n"})}),"\n",(0,s.jsxs)(n.p,{children:["The C# wrapper used the C# best practise syntax ",(0,s.jsx)(n.code,{children:"@class"}),", which worked fine for me."]}),"\n",(0,s.jsx)(n.h4,{id:"c---member-name--enclosing-type",children:"C# - Member name = enclosing type"}),"\n",(0,s.jsxs)(n.p,{children:["I encountered this issue first for ",(0,s.jsx)(n.code,{children:"block.block"}),". In C# member names are not allowed to be the same as the enclosing type."]}),"\n",(0,s.jsxs)(n.p,{children:["As i am not a very experienced C# developer, i am still rather undecided on how to handle this issue.\nSo far I have been using ",(0,s.jsx)(n.code,{children:"Block.block"}),", as I am thinking about using PascalCase for all classes in C#. And if only to mute the warnings in VS Community."]}),"\n",(0,s.jsx)(n.p,{children:"If you have a strong opinion about this topic, please let me know! I am interested in hearing your thoughts."}),"\n",(0,s.jsx)(n.h4,{id:"transpiled-names",children:"Transpiled names"}),"\n",(0,s.jsxs)(n.p,{children:["And back to ",(0,s.jsx)(n.code,{children:"classDiagram.``class`` "}),". While JavaScript does not seem to care about this topic to much, Python does."]}),"\n",(0,s.jsxs)(n.p,{children:["JavaScript gives us the best result, the F# backtick syntax is transpiled to a simple camelCase name ",(0,s.jsx)(n.code,{children:"classDiagram.class"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Python on the other hand has ",(0,s.jsx)(n.code,{children:"class"})," also as reserved keyword. Fable transpiles it to ",(0,s.jsx)(n.code,{children:"classDiagram.class_"}),".\nWhich raises the question if i should simply apply this syntax to all cases with naming problems."]}),"\n",(0,s.jsx)(n.h3,{id:"docsnative-test-maintainance",children:"Docs/Native Test Maintainance"}),"\n",(0,s.jsxs)(n.p,{children:["The core library + C# wrapper were done rather quickly. I can also recycle my F# unit tests to check if transpilation works as expected, using ",(0,s.jsx)(n.a,{href:"https://github.com/Freymaurer/Fable.Pyxpecto",children:"Fable.Pyxpecto"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"But testing correct native accessibillity and writing docs (showcasing the test cases) was the most time consuming part and something i am not happy with."}),"\n",(0,s.jsx)(n.p,{children:"Here are some ideas on how to improve this:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Theoretically, i could use the transpiled tests for docs. But i still have to remove the Fable specific helper functions and replace them with native ones."}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.em,{children:(0,s.jsx)(n.a,{href:"https://github.com/kMutagene",children:"Kevin S."})})," had an idea, repurposing jupyter notebooks for docs and testing. To at least unify the testing and docs."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"If you have any ideas on how to improve this, please let me know!"}),"\n"]})}function m(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(p,{...e})}):p(e)}},9365:(e,n,t)=>{t.d(n,{A:()=>r});t(6540);var s=t(4164);const a={tabItem:"tabItem_Ymn6"};var i=t(4848);function r(e){let{children:n,hidden:t,className:r}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,s.A)(a.tabItem,r),hidden:t,children:n})}},1470:(e,n,t)=>{t.d(n,{A:()=>w});var s=t(6540),a=t(4164),i=t(3104),r=t(6347),l=t(205),o=t(7485),c=t(1682),h=t(9466);function d(e){return s.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function p(e){const{values:n,children:t}=e;return(0,s.useMemo)((()=>{const e=n??function(e){return d(e).map((e=>{let{props:{value:n,label:t,attributes:s,default:a}}=e;return{value:n,label:t,attributes:s,default:a}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function m(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function u(e){let{queryString:n=!1,groupId:t}=e;const a=(0,r.W6)(),i=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,o.aZ)(i),(0,s.useCallback)((e=>{if(!i)return;const n=new URLSearchParams(a.location.search);n.set(i,e),a.replace({...a.location,search:n.toString()})}),[i,a])]}function g(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,i=p(e),[r,o]=(0,s.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!m({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const s=t.find((e=>e.default))??t[0];if(!s)throw new Error("Unexpected error: 0 tabValues");return s.value}({defaultValue:n,tabValues:i}))),[c,d]=u({queryString:t,groupId:a}),[g,f]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,i]=(0,h.Dv)(t);return[a,(0,s.useCallback)((e=>{t&&i.set(e)}),[t,i])]}({groupId:a}),y=(()=>{const e=c??g;return m({value:e,tabValues:i})?e:null})();(0,l.A)((()=>{y&&o(y)}),[y]);return{selectedValue:r,selectValue:(0,s.useCallback)((e=>{if(!m({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);o(e),d(e),f(e)}),[d,f,i]),tabValues:i}}var f=t(2303);const y={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var b=t(4848);function x(e){let{className:n,block:t,selectedValue:s,selectValue:r,tabValues:l}=e;const o=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),h=e=>{const n=e.currentTarget,t=o.indexOf(n),a=l[t].value;a!==s&&(c(n),r(a))},d=e=>{let n=null;switch(e.key){case"Enter":h(e);break;case"ArrowRight":{const t=o.indexOf(e.currentTarget)+1;n=o[t]??o[0];break}case"ArrowLeft":{const t=o.indexOf(e.currentTarget)-1;n=o[t]??o[o.length-1];break}}n?.focus()};return(0,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":t},n),children:l.map((e=>{let{value:n,label:t,attributes:i}=e;return(0,b.jsx)("li",{role:"tab",tabIndex:s===n?0:-1,"aria-selected":s===n,ref:e=>o.push(e),onKeyDown:d,onClick:h,...i,className:(0,a.A)("tabs__item",y.tabItem,i?.className,{"tabs__item--active":s===n}),children:t??n},n)}))})}function j(e){let{lazy:n,children:t,selectedValue:a}=e;const i=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=i.find((e=>e.props.value===a));return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:i.map(((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==a})))})}function v(e){const n=g(e);return(0,b.jsxs)("div",{className:(0,a.A)("tabs-container",y.tabList),children:[(0,b.jsx)(x,{...e,...n}),(0,b.jsx)(j,{...e,...n})]})}function w(e){const n=(0,f.A)();return(0,b.jsx)(v,{...e,children:d(e.children)},String(n))}},8453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>l});var s=t(6540);const a={},i=s.createContext(a);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/831440af.c04aa112.js b/assets/js/831440af.c04aa112.js new file mode 100644 index 0000000..aacccbc --- /dev/null +++ b/assets/js/831440af.c04aa112.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[7466],{4463:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>h,contentTitle:()=>o,default:()=>m,frontMatter:()=>l,metadata:()=>c,toc:()=>d});var s=t(4848),a=t(8453),i=t(1470),r=t(9365);const l={slug:"Library Design",title:"Library Design",authors:"freyk",tags:["fable","library","api","design","helper"]},o="Libary Design",c={permalink:"/Siren/blog/Library Design",editUrl:"https://github.com/Freymaurer/Siren/tree/main/docs/blog/2024-05-06-fable-library-design.mdx",source:"@site/blog/2024-05-06-fable-library-design.mdx",title:"Library Design",description:"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#.",date:"2024-05-06T00:00:00.000Z",tags:[{label:"fable",permalink:"/Siren/blog/tags/fable"},{label:"library",permalink:"/Siren/blog/tags/library"},{label:"api",permalink:"/Siren/blog/tags/api"},{label:"design",permalink:"/Siren/blog/tags/design"},{label:"helper",permalink:"/Siren/blog/tags/helper"}],readingTime:23.63,hasTruncateMarker:!1,authors:[{name:"Kevin Frey",title:"Maintainer of Siren",url:"https://github.com/Freymaurer",imageURL:"https://github.com/Freymaurer.png",key:"freyk"}],frontMatter:{slug:"Library Design",title:"Library Design",authors:"freyk",tags:["fable","library","api","design","helper"]},unlisted:!1},h={authorsImageUrls:[void 0]},d=[{value:"Idea \ud83d\udca1",id:"idea-",level:2},{value:"What is Fable?",id:"what-is-fable",level:2},{value:"Benefits",id:"benefits",level:3},{value:"API Design",id:"api-design",level:2},{value:"Using [<AttachMembers>]",id:"using-attachmembers",level:3},{value:"C# Compatibility",id:"c-compatibility",level:3},{value:"Code generator helper",id:"code-generator-helper",level:4},{value:"Maintainability",id:"maintainability",level:4},{value:"Python/JavaScript import and Index files",id:"pythonjavascript-import-and-index-files",level:3},{value:"Publish",id:"publish",level:2},{value:".NET",id:"net",level:3},{value:"Pypi",id:"pypi",level:3},{value:"JavaScript (+Types)",id:"javascript-types",level:3},{value:"Deep Dive",id:"deep-dive",level:2},{value:"Overloads",id:"overloads",level:3},{value:"Erased Unions",id:"erased-unions",level:4},{value:"JavaScript optional parameters",id:"javascript-optional-parameters",level:3},{value:"Member Names",id:"member-names",level:3},{value:"F# reserved keywords",id:"f-reserved-keywords",level:4},{value:"C# - Member name = enclosing type",id:"c---member-name--enclosing-type",level:4},{value:"Transpiled names",id:"transpiled-names",level:4},{value:"Docs/Native Test Maintainance",id:"docsnative-test-maintainance",level:3}];function p(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",input:"input",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#."}),"\n",(0,s.jsx)(n.admonition,{title:"Fable.Multiverse",type:"tip",children:(0,s.jsxs)(n.p,{children:["To make it as easy as possible to create a Fable library that publishes in multiple languages, I created a template called ",(0,s.jsx)(n.a,{href:"https://github.com/Freymaurer/Fable.Multiverse",children:"Fable.Multiverse"})," ","\ud83c\udf89"]})}),"\n",(0,s.jsx)(n.h2,{id:"idea-",children:"Idea \ud83d\udca1"}),"\n",(0,s.jsxs)(n.p,{children:["During a hackathon for our research data management consortium, we were discussing ideas for visualizing graph like structures in a way that allows easy gitlab integration and can be understood and used by any level of user. The idea we pursued was to add ",(0,s.jsx)(n.code,{children:".md"})," files with mermaid graphs for an easy overview. So why not write a domain specific language for mermaid graphs to make creation of such graphs easier and more error proof. Good idea, but what programming language should we use? In our consortium we have several groups, some using JavaScript, some Python, some (us) .NET or more specifically F#. Because i already have quite some experience using ",(0,s.jsx)(n.a,{href:"https://fable.io",children:"Fable"}),", i did the mental checklist to see if it would be a good fit for this project."]}),"\n",(0,s.jsxs)(n.ul,{className:"contains-task-list",children:["\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Does not need any dependencies."})," Mermaid graphs, are mostly YAML, so no complex syntax."]}),"\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Does not require IO interaction."})," We can simply focus on writing our mermaid graph as string and allow the user to do whatever they want with it."]}),"\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Only Fable compatible languages needed."})," We are already very happy offering such a tool in Python, JavaScript and F#."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:".. and thats it \ud83c\udf89 So we can start developing a libary with one codebase for 4 [5] languages."}),"\n",(0,s.jsx)(n.h2,{id:"what-is-fable",children:"What is Fable?"}),"\n",(0,s.jsxs)(n.p,{children:["Fable is a F# to X transpiler. It started out targeting only JavaScript, using a naming reference to the popular ",(0,s.jsx)(n.a,{href:"https://babeljs.io",children:"Babel"})," JavaScript transpiler. Now Fable aims to support multiple languages, all in different states. At the time of writing, the offical Fable docs state the following:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Language"}),(0,s.jsx)(n.th,{children:"Status"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JavaScript"}),(0,s.jsx)(n.td,{children:"Stable"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"TypeScript"}),(0,s.jsx)(n.td,{children:"Stable"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Dart"}),(0,s.jsx)(n.td,{children:"Beta"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Python"}),(0,s.jsx)(n.td,{children:"Beta"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Rust"}),(0,s.jsx)(n.td,{children:"Alpha"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"PHP"}),(0,s.jsx)(n.td,{children:"Experimental"})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"benefits",children:"Benefits"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Type Safety"}),". F# is a statically typed language, which means that the compiler can catch many errors before they even happen."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Lightweight Syntax"}),". F# has a very lightweight syntax, which makes it easy to read and write. It does not require a lot of boilerplate code and you can get right into the meat of your program."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Testing(?)"}),". Main codebase is written in F# as well as most tests. This allows us to also transpile the tests to other languages and run them there. This is a big advantage, as we can be sure that the tests are the same in all languages."]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{title:"But why the question mark behind testing?",type:"note",children:(0,s.jsx)(n.p,{children:"Because we can recycle the tests, to ensure correct functionality, but we still must test if the library can be used from all supported languages without hurdles."})}),"\n",(0,s.jsx)(n.h2,{id:"api-design",children:"API Design"}),"\n",(0,s.jsx)(n.p,{children:"To make the code look and feel as native as possible in all languages, there are some things we need to consider. But first let us have a look at fable transpiled code."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["The following code will use the ",(0,s.jsx)(n.a,{href:"https://fable.io/repl/",children:"Fable REPL"})," to transpile code for easy showcasing!"]})}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'let helloWorld = printfn "Hello World"\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { printf, toConsole } from "fable-library-js/String.js";\n\nexport const helloWorld = toConsole(printf("Hello World"));\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'from fable_library_js.string import (to_console, printf)\n\nhello_world: None = to_console(printf("Hello World"))\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"We can already notice some things:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Fable tries to transpile into native syntax, so for example snake_case in Python and camelCase in JavaScript"}),"\n",(0,s.jsxs)(n.li,{children:["Fable has some wrappers for functions which might have native equivalents. In F# ",(0,s.jsx)(n.code,{children:"printfn"})," is used to print to the console, in JavaScript ",(0,s.jsx)(n.code,{children:"console.log"})," and in Python ",(0,s.jsx)(n.code,{children:"print"}),". But Fable uses their own printf function to ensure 100% correct f# transpilation."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Next we will have a look at a class with some member functions."}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"type MyClass =\n static member add (x: int) (y: int) = x + y\n"})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n}\n\nexport function MyClass_$reflection() {\n return class_type("Test.MyClass", undefined, MyClass);\n}\n\nexport function MyClass_add(x, y) {\n return x + y;\n}\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'from fable_library_js.reflection import (TypeInfo, class_type)\n\ndef _expr0() -> TypeInfo:\n return class_type("Test.MyClass", None, MyClass)\n\n\nclass MyClass:\n ...\n\nMyClass_reflection = _expr0\n\ndef MyClass_add(x: int, y: int) -> int:\n return x + y\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Oh no, this does not look good. Fable does a thing called ",(0,s.jsx)(n.a,{href:"https://fable.io/docs/javascript/features.html#name-mangling",children:(0,s.jsx)(n.em,{children:"name mangling"})}),".\nHave a look at the offical docs for a deeper view on this topic.\nFor now its enought to know, this is done to allow overloading functions in F#."]}),"\n",(0,s.jsx)(n.admonition,{title:"Edit- Tree-Shaking",type:"info",children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.a,{href:"https://github.com/ncave",children:"ncave"})," pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members."]})}),"\n",(0,s.jsxs)(n.h3,{id:"using-attachmembers",children:["Using ",(0,s.jsx)(n.code,{children:"[]"})]}),"\n",(0,s.jsx)(n.p,{children:"But we can tell Fable that we know what we are doing and ignore name mangling."}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"open Fable.Core\n\n[]\ntype MyClass =\n static member add (x: int) (y: int) = x + y\n"})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static add(x, y) {\n return x + y;\n }\n}\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:"from fable_library_js.reflection import (TypeInfo, class_type)\n\nclass MyClass:\n @staticmethod\n def add(x: int, y: int) -> int:\n return x + y\n"})})})]}),"\n",(0,s.jsxs)(n.p,{children:["That looks better and allows us to do the following in all 3 languages: ",(0,s.jsx)(n.code,{children:"MyClass.add"}),".\nThis is the basic design i chose to use for most user facing api. All F#/Fable code not easily usable from other languages is hidden behind a facade like this."]}),"\n",(0,s.jsx)(n.h3,{id:"c-compatibility",children:"C# Compatibility"}),"\n",(0,s.jsxs)(n.p,{children:["Strangely enough allowing C# users the same ease of use as Python and JavaScript users is the hardest.\nThis is because C# has some issues with F# ",(0,s.jsx)(n.em,{children:"optional parameters"})," and F# ",(0,s.jsx)(n.em,{children:"tuples"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"In F# we can define a function like this:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'[]\ntype flowchart =\n static member raw (txt: string) = FlowchartElement txt\n static member id (txt: string) = FlowchartElement txt\n static member node (id: string, ?name: string) : FlowchartElement = ...\n\nflowchart.node("My id")\nflowchart.node("My id", "My name")\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Using the F# function in C# will result in an error, when you try to do ",(0,s.jsx)(n.code,{children:'flowchart.node("My id")'}),", as ",(0,s.jsx)(n.code,{children:"?name"})," is a ",(0,s.jsx)(n.code,{children:"Microsoft.FSharp.Core.FSharpOption"})," without any default information."]}),"\n",(0,s.jsx)(n.p,{children:"By creating a C# access layer we can avoid this issue for C# users:"}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["The C# extensions for optional parameters and tuples are taken from ",(0,s.jsx)(n.a,{href:"https://github.com/plotly/Plotly.NET",children:"Plotly.NET"})," with the help from my dear colleague ",(0,s.jsx)(n.a,{href:"https://github.com/kMutagene",children:"Kevin Schneider"}),"."]})}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"flow",label:"Flowchart.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public static class flowchart\n{\n public static FlowchartElement raw(string txt) => Siren.flowchart.raw(txt);\n\n public static FlowchartElement id(string txt) => Siren.flowchart.raw(txt);\n\n public static FlowchartElement node(string id, Optional name = default) =>\n Siren.flowchart.node(id, name.ToOption());\n //...\n}\n"})})}),(0,s.jsx)(r.A,{value:"fsharp",label:"OptionExtension.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'/// \n/// Helper type for handling the special way the Plotly.NET core API uses generics.\n/// In short, the problem arises because many optional parameters of Plotly.NET\'s core API are generics \n/// with a type constraint for `IConvertible`. This means that these parameters can be both value and reference types \n/// (e.g. `double` and `System.DateTime` both implement IConvertible).\n/// If we now have a optional parameter of type `T? where T: IConvertible` the compiler will not allow this \n/// without further type constrainst to eithe reference or value type.\n/// This is a problem because we want to 1. allow both, and 2. have a reliable way of determining if the value was not set \n/// because the F# API expects to be passed `Option.None` in that case.\n/// There exist other workarounds like checking if the value is default or null, but that changes valid default values actually set to null as well.\n/// \n/// \n/// The value to mark as optional\n/// Wether or not the wrapped value is valid. This is used downstream to determine wether to wrap this value into `Option.Some` (if true) or `Option.None` (if false)\npublic readonly record struct Optional(T Value, bool IsSome)\n{\n /// \n /// \n /// \n /// \n public static implicit operator Optional(T Value) => new(Value, true);\n\n}\n/// \n/// Extension methods for the `Optional` class\n/// \npublic static class OptionalExtensions\n{\n /// \n /// Converts the `Optional` value to `Some(value)` if the value is valid, or `None` if it is not.\n /// \n /// \n /// The `Optional` value to convert to a F# Option\n /// opt converted to `Option`\n static internal Microsoft.FSharp.Core.FSharpOption ToOption(this Optional opt) => opt.IsSome ? new(opt.Value) : Microsoft.FSharp.Core.FSharpOption.None;\n}\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"A similar issue arises with F# tuples:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"flow",label:"Flowchart.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"[]\ntype flowchart =\n static member stylesNode (nodeId: string, styles:#seq) = Flowchart.formatNodeStyles [nodeId] (List.ofSeq styles) |> FlowchartElement\n"})})}),(0,s.jsx)(r.A,{value:"extension",label:"TupleExtension.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"/// \n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\n/// \ninternal static class TupleExtensions\n{\n /// \n /// Converts a tuple.\n /// \n internal static Tuple ToTuple(this ValueTuple t) => Tuple.Create(t.Item1, t.Item2);\n}\n"})})}),(0,s.jsx)(r.A,{value:"fsharp",label:"Flowchart.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"/// \n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\n/// \npublic static class flowchart\n{\n public static FlowchartElement stylesNode(string nodeId, (string, string)[] styles) =>\n Siren.flowchart.stylesNode(nodeId, styles.Select(t => t.ToTuple()));\n}\n"})})})]}),"\n",(0,s.jsx)(n.h4,{id:"code-generator-helper",children:"Code generator helper"}),"\n",(0,s.jsx)(n.p,{children:"These incompatibilities are not only annoying but providing a consistently native C# experience, requires a wrapping for all apis.\nTo make this easier, i created a code generator that takes a F# file and generates the C# wrapper for it. .. Or at least 95% of it. The rest is done by hand."}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Code Generator"}),(0,s.jsx)("div",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open System.Reflection\n\n[]\nlet FSharpOptionDefault = "Optional"\n\nlet transformParameterTypeName (paramTypeName: string)=\n match paramTypeName with\n | "String" -> "string"\n | "Int32" -> "int"\n | "Double" -> "double"\n | "FSharpOption`1" -> FSharpOptionDefault // this is not always true but a good approximation\n | "Tuple`2" -> "(string,string)" // this is not always true but a good approximation\n | "Boolean" -> "bool"\n | _ -> paramTypeName\n\ntype ParameterInfo = {\n Type: string\n Name: string\n} with\n member this.FSharpParam =\n match this with\n | {Type = FSharpOptionDefault} -> this.Name + ".ToOption()"\n | _ -> this.Name\n member this.CSharpParam =\n match this with\n | {Type = FSharpOptionDefault} -> sprintf "%s %s = default" this.Type this.Name\n | _ -> sprintf "%s %s" this.Type this.Name\n\n static member create(typeName: string, name: string) =\n {Type = transformParameterTypeName typeName; Name = name}\n\nlet generateCSharpCode<\'A>() =\n\n let t = typeof<\'A>\n let members = t.GetMethods(BindingFlags.Static ||| BindingFlags.Public)\n\n let mutable csharpCode = sprintf "public static class %s\\n{\\n" t.Name\n for m in members do\n let methodName = \n let name0 = m.Name\n if name0.StartsWith("get_") then\n name0.Substring(4)\n else\n name0\n let returnType = m.ReturnType.Name\n let params0 = \n m.GetParameters() \n |> Array.map (fun p -> ParameterInfo.create(p.ParameterType.Name, p.Name))\n let csharpParameters =\n if params0.Length = 0 then\n ""\n else \n params0\n |> Array.map _.CSharpParam\n |> String.concat(", ")\n |> fun s -> "(" + s + ")"\n \n let fsharpParameters = \n if params0.Length = 0 then\n ""\n else \n params0 \n |> Array.map _.FSharpParam\n |> String.concat(", ")\n |> fun s -> "(" + s + ")"\n \n let methodSignature = $"public static {transformParameterTypeName returnType} {methodName}{csharpParameters}"\n let methodBody = \n if methodName.StartsWith("get_") then\n let withoutGet = methodName.Substring(4)\n $"return Siren.{t.Name}.{withoutGet};"\n else\n $" => Siren.{t.Name}.{methodName}{fsharpParameters};"\n csharpCode <- csharpCode + $" {methodSignature}\\n {methodBody}\\n"\n\n csharpCode <- csharpCode + "}\\n"\n csharpCode\n\nlet test() = \n generateCSharpCode() // Here you can pass any type you want to generate C# code for\n |> printfn "%A"\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"This is something i did not want to spend a lot of time on, so when i quickly wrote this and noticed it was able to create most of the C# code correctly.\nI think improving this code to create everything perfectly would be awesome, but would have taken me longer thant fixing the few mistakes it makes by hand."}),"\n",(0,s.jsx)(n.h4,{id:"maintainability",children:"Maintainability"}),"\n",(0,s.jsx)(n.p,{children:"This is a big issue. Whenever i update my f# api i must also update the c# wrapper.\nChanges are mostly catched by the compiler but missings functions are not."}),"\n",(0,s.jsx)(n.p,{children:"That is why i added unit tests to check the count and name of a c# and f# class and compare it for equality:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public static class Utils\n{\n public static int GetMemberCount(Type type)\n {\n var members = type.GetMembers();\n return members.Length;\n }\n\n public static List GetMemberNameDifferences(Type type1, Type type2)\n {\n List differences = new List();\n //transform string to lower\n var type1Members = type1.GetMembers().Select(m => m.Name.ToLower());\n var type2Members = type2.GetMembers().Select(m => m.Name.ToLower());\n differences.AddRange(type1Members.Except(type2Members));\n differences.AddRange(type2Members.Except(type1Members));\n\n return differences;\n }\n\n public static void CompareClasses(Type csharpType, Type fsharpType)\n {\n int csharpMemberCount = GetMemberCount(csharpType);\n int fsharpMemberCount = GetMemberCount(fsharpType);\n List differences = GetMemberNameDifferences(fsharpType, csharpType);\n\n Assert.Empty(differences);\n Assert.Equal(fsharpMemberCount, csharpMemberCount);\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"This at least helps me to catch missing functions in the c# wrapper, even if i still have to write them by hand."}),"\n",(0,s.jsx)(n.h3,{id:"pythonjavascript-import-and-index-files",children:"Python/JavaScript import and Index files"}),"\n",(0,s.jsxs)(n.p,{children:["F# and C# use namespaces to organize code. I can have multiple files with the same namespace and access all functions simply by writing ",(0,s.jsx)(n.code,{children:"open Siren"}),"/",(0,s.jsx)(n.code,{children:"using Siren.Sea;"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"In Python and JavaScript this is not possible. Here imports happen on a file basis. So i needed to have a single file that imports all other files and exports them."}),"\n",(0,s.jsx)(n.p,{children:"Luckily this file can be created automatically!"}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Index File"}),(0,s.jsx)("div",{children:(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"util",label:"Util.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.Util\n\nopen System\nopen System.IO\nopen System.Text.RegularExpressions\n\ntype FileInformation = {\n FilePath : string\n Lines : string []\n} with\n static member create(filePath: string, lines: string []) = {\n FilePath = filePath\n Lines = lines\n }\n\nlet getAllFiles(path: string, extension: string) = \n let options = EnumerationOptions()\n options.RecurseSubdirectories <- true\n IO.Directory.EnumerateFiles(path,extension,options)\n |> Seq.filter (fun s -> s.Contains("fable_modules") |> not)\n |> Array.ofSeq\n\nlet findClasses (rootPath: string) (cois: string []) (regexPattern: string -> string) (filePaths: seq) = \n let files = [|\n for fp in filePaths do\n yield FileInformation.create(fp, System.IO.File.ReadAllLines (fp))\n |]\n let importStatements = ResizeArray()\n let findClass (className: string) = \n /// maybe set this as default if you do not want any whitelist\n let classNameDefault = @"[a-zA-Z_0-9]"\n let pattern = Regex.Escape(className) |> regexPattern\n let regex = Regex(pattern)\n let mutable found = false\n let mutable result = None\n let enum = files.GetEnumerator()\n while not found && enum.MoveNext() do\n let fileInfo = enum.Current :?> FileInformation\n for line in fileInfo.Lines do\n let m = regex.Match(line)\n match m.Success with\n | true -> \n found <- true\n result <- Some <| (className, IO.Path.GetRelativePath(rootPath,fileInfo.FilePath))\n | false ->\n ()\n match result with\n | None ->\n failwithf "Unable to find %s" className\n | Some r ->\n importStatements.Add r\n for coi in cois do findClass coi\n importStatements\n |> Array.ofSeq\n\nlet writeIndexFile (path: string) (fileName: string) (content: string) =\n let filePath = Path.Combine(path, fileName)\n File.WriteAllText(filePath, content)\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"Indexjs.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.JS\n\nlet private getAllJsFiles (path: string) fileExtension = \n Util.getAllFiles(path,$"*.{fileExtension}")\n\nlet private pattern (className: string) = sprintf @"^export class (?%s)+[\\s{].*({)?" className\n\nlet private findJsClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \n Util.findClasses rootPath whiteList pattern filePaths\n\nopen System.Text\n\nlet private createImportStatements (info: (string*string) []) =\n let sb = StringBuilder()\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\n for filePath, imports in importCollection do\n let p = filePath.Replace("\\\\","/").Replace("ts","js")\n sb.Append "export { " |> ignore\n sb.AppendJoin(", ", imports) |> ignore\n sb.Append " } from " |> ignore\n sb.Append (sprintf "\\"./%s\\"" p) |> ignore\n sb.Append ";" |> ignore\n sb.AppendLine() |> ignore\n sb.ToString()\n\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string [], fileExtension: string) =\n getAllJsFiles rootPath fileExtension\n |> findJsClasses rootPath whiteList\n |> createImportStatements\n |> Util.writeIndexFile rootPath fileName\n\nlet generate(rootPath: string) (ts: bool) = \n let extension = if ts then "ts" else "js"\n generateIndexfile(rootPath, $"index.{extension}", WhiteList.WhiteList, extension)\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Indexpy.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'let private getAllJsFiles(path: string) = \n Util.getAllFiles(path,"*.py")\n\nlet private pattern (className: string) = sprintf @"^class (?%s)+(\\(|:).*$" className\n\nlet private findPyClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \n Util.findClasses rootPath whiteList pattern filePaths\n\nopen System.Text\n\nlet private createImportStatements (info: (string*string) []) =\n let sb = StringBuilder()\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\n for filePath, imports in importCollection do\n let p = filePath |> Path.GetFileNameWithoutExtension\n sb.Append (sprintf "from .%s import " p) |> ignore\n sb.AppendJoin(", ", imports) |> ignore\n sb.AppendLine() |> ignore\n sb.ToString()\n\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string []) =\n getAllJsFiles(rootPath)\n |> findPyClasses rootPath whiteList\n |> createImportStatements\n |> Util.writeIndexFile rootPath fileName\n\nlet generate(rootPath: string) (name: string) = \n // code to make camelcase to snakecase\n /// This is because we currently snake_case everything that does not start with a capital letter\n let camelCaseToSnakeCase (str: string) = \n if Char.IsUpper str.[0] then\n str \n else\n str \n |> Seq.fold (fun (acc: string) c -> \n if Char.IsUpper c then \n acc + "_" + string (Char.ToLower c) \n else \n acc + string c\n ) ""\n let snake_case_white_list = WhiteList.WhiteList |> Array.map camelCaseToSnakeCase\n generateIndexfile(rootPath, name, snake_case_white_list)\n'})})}),(0,s.jsx)(r.A,{value:"whitelist",label:"WhiteList.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.WhiteList\n\nlet WhiteList = [|\n "SirenElement"\n // ThemeVaruiables\n "themeVariable"\n "quadrantTheme"; \n "gitTheme"; \n "timelineTheme"; \n "xyChartTheme"; \n "pieTheme";\n // Config\n "graphConfig";\n "flowchartConfig"; \n "sequenceConfig"; \n "ganttConfig"; \n "journeyConfig"; \n "timelineConfig"; \n "classConfig"; \n "stateConfig"; \n "erConfig"; \n "quadrantChartConfig"; \n "pieConfig"; \n "sankeyConfig"; \n "xyChartConfig"; \n "mindmapConfig"; \n "gitGraphConfig"; \n "requirementConfig"; \n // Graphs and Helpers\n "formatting"; \n "direction"; \n "flowchart"; \n "notePosition"; \n "sequence"; \n "classMemberVisibility"; \n "classMemberClassifier"; \n "classDirection"; \n "classCardinality"; \n "classRltsType"; \n "classDiagram"; \n "stateDiagram"; \n "erKey"; \n "erCardinality"; \n "erDiagram"; \n "journey"; \n "ganttTime"; \n "ganttTags"; \n "ganttUnit"; \n "gantt"; \n "pieChart"; \n "quadrant"; \n "rqRisk"; \n "rqMethod"; \n "requirement"; \n "gitType"; \n "git"; \n "mindmap"; \n "timeline"; \n "sankey"; \n "xyChart"; \n "block"; \n "theme"; \n "siren"; \n|]\n'})})})]})})]}),"\n",(0,s.jsx)(n.p,{children:"The resulting files look like this:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"js",label:"index.js",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'export { SirenElement } from "./SirenTypes.js";\nexport { themeVariable, quadrantTheme, gitTheme, timelineTheme, xyChartTheme, pieTheme } from "./ThemeVariables.js";\nexport { graphConfig, flowchartConfig, sequenceConfig, ganttConfig, journeyConfig, timelineConfig, classConfig, stateConfig, erConfig, quadrantChartConfig, pieConfig, sankeyConfig, xyChartConfig, mindmapConfig, gitGraphConfig, requirementConfig } from "./Config.js";\nexport { formatting, direction, flowchart, notePosition, sequence, classMemberVisibility, classMemberClassifier, classDirection, classCardinality, classRltsType, classDiagram, stateDiagram, erKey, erCardinality, erDiagram, journey, ganttTime, ganttTags, ganttUnit, gantt, pieChart, quadrant, rqRisk, rqMethod, requirement, gitType, git, mindmap, timeline, sankey, xyChart, block, theme, siren } from "./Siren.js";\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"index.py/__init__.py",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-py",children:"from .siren_types import SirenElement\nfrom .theme_variables import theme_variable, quadrant_theme, git_theme, timeline_theme, xy_chart_theme, pie_theme\nfrom .config import graph_config, flowchart_config, sequence_config, gantt_config, journey_config, timeline_config, class_config, state_config, er_config, quadrant_chart_config, pie_config, sankey_config, xy_chart_config, mindmap_config, git_graph_config, requirement_config\nfrom .siren import formatting, direction, flowchart, note_position, sequence, class_member_visibility, class_member_classifier, class_direction, class_cardinality, class_rlts_type, class_diagram, state_diagram, er_key, er_cardinality, er_diagram, journey, gantt_time, gantt_tags, gantt_unit, gantt, pie_chart, quadrant, rq_risk, rq_method, requirement, git_type, git, mindmap, timeline, sankey, xy_chart, block, theme, siren\n"})})})]}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["Python packages will default import whatever is in the ",(0,s.jsx)(n.code,{children:"__init__.py"})," file. So we must simply name the index file for python as such (at least for publishing)."]})}),"\n",(0,s.jsx)(n.h2,{id:"publish",children:"Publish"}),"\n",(0,s.jsx)(n.p,{children:"As soon as we get to the publishing step everything is back to standard for the respective languages."}),"\n",(0,s.jsx)(n.h3,{id:"net",children:".NET"}),"\n",(0,s.jsxs)(n.p,{children:["The easiest. We already have the required project files, no need to transpile. So we can simply use ",(0,s.jsx)(n.code,{children:"dotnet pack"})," to create .nupkg files.\nThen ",(0,s.jsx)(n.code,{children:"dotnet nuget push"})," to push to ",(0,s.jsx)(n.a,{href:"https://www.nuget.org",children:"nuget.org"}),"."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h3,{id:"pypi",children:"Pypi"}),"\n",(0,s.jsxs)(n.p,{children:["Also quite easy. Transpile f# code to python code, copy ",(0,s.jsx)(n.code,{children:"pyproject.toml"})," and ",(0,s.jsx)(n.code,{children:"README.md"})," into the ",(0,s.jsx)(n.code,{children:"dist"})," folder and create ",(0,s.jsx)(n.code,{children:"index.py"})," file.\nRun ",(0,s.jsx)(n.code,{children:"python -m poetry build"})," to create a publishing files. Then publish files with ",(0,s.jsx)(n.code,{children:"python -m poetry publish"})]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h3,{id:"javascript-types",children:"JavaScript (+Types)"}),"\n",(0,s.jsxs)(n.p,{children:["For this i followed the Example of ",(0,s.jsx)(n.a,{href:"https://fable.io/blog/2023/2023-04-20-Better_Typed_than_Sorry.html",children:"Better Typed than Sorry"})," by ",(0,s.jsx)(n.a,{href:"https://github.com/alfonsogarciacaro",children:"Alfonso Garc\xeda-Caro"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Transpile F# to TypeScript, then use ",(0,s.jsx)(n.code,{children:"tsc"})," to transpile TypeScript to JavaScript and type information files (",(0,s.jsx)(n.code,{children:"*.d.ts"}),"). Add the ",(0,s.jsx)(n.code,{children:"index.js"})," file to the ",(0,s.jsx)(n.code,{children:"dist folder"}),"\nThen publish to npm with ",(0,s.jsx)(n.code,{children:"npm publish"}),"."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h2,{id:"deep-dive",children:"Deep Dive"}),"\n",(0,s.jsx)(n.p,{children:"From here on are some additional issues i encountered during development."}),"\n",(0,s.jsx)(n.h3,{id:"overloads",children:"Overloads"}),"\n",(0,s.jsxs)(n.p,{children:["The concept of allowing different inputs for the same function exists in f#, as well as python and javascript.\nBy using the ",(0,s.jsx)(n.code,{children:"[]"})," attribute we are not longer allowed to use standard f# overloads.\nBecause JavaScript does not have the same kind of type interference it is unable to recognice which function should be invoked:"]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\n\n[]\ntype MyClass =\n static member add (x: int, y: int) = x * y // this should be invoked\n static member add (x: string, y: string) = x + y\n\nlet result = MyClass.add(10, 20)\n\nprintfn "%A" result\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\nimport { printf, toConsole } from "fable-library-js/String.js";\n\nexport class MyClass {\n constructor() {\n }\n static add(x, y) {\n return x * y;\n }\n static add(x, y) { // This shadows the function above and is invoked\n return x + y;\n }\n}\n\nexport const result = MyClass.add(10, 20); // This will return 30\n\ntoConsole(printf("%A"))(result);\n\n'})})})]}),"\n",(0,s.jsx)(n.h4,{id:"erased-unions",children:"Erased Unions"}),"\n",(0,s.jsxs)(n.p,{children:["It is possible to imitate js overload behaviour by using ",(0,s.jsx)(n.a,{href:"https://fable.io/docs/javascript/features.html#erased-unions",children:"erased unions"}),".\nHere we use a Fable provides discriminate union called ",(0,s.jsx)(n.code,{children:"U2"}),"(",(0,s.jsx)(n.code,{children:"U3"}),", ",(0,s.jsx)(n.code,{children:"U4"}),"...). After transpilation it is replaced by a js type check."]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\nopen Fable.Core.JsInterop\n\n[]\ntype MyClass =\n static member test (arg: U2) = \n match arg with\n | U2.Case1 s -> printfn "This is a string: %s" s\n | U2.Case2 i -> printfn "This is a integer: %i" i\n\nlet result = MyClass.test(U2.Case2 10) // or MyClass.test(!^10)\n\nprintfn "%A" result\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { printf, toConsole } from "fable-library-js/String.js";\nimport { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static test(arg) {\n if (typeof arg === "number") {\n const i = arg;\n toConsole(printf("This is a integer: %i"))(i);\n }\n else {\n const s = arg;\n toConsole(printf("This is a string: %s"))(s);\n }\n }\n}\n\nexport const result = MyClass.test(10);\n\ntoConsole(printf("%A"))(); // This is a integer: 10\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"As you can see, this looks really nice in JavaScript, but is cumbersome to use in F#."}),"\n",(0,s.jsx)(n.p,{children:"It would be possible to do do both and use f# overloads and shadow them with the erased union. But this would add more additional maintainance work."}),"\n",(0,s.jsxs)(n.admonition,{title:"Compiler Statements",type:"info",children:[(0,s.jsxs)(n.p,{children:["We can use ",(0,s.jsx)(n.code,{children:"#if FABLE_COMPILER ... #else ... #endif"})," syntax to include code only in certain compiler states."]}),(0,s.jsx)(n.p,{children:"In the following example the erased union is only used (and accessible!) when the code is transpiled by Fable to JavaScript."})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'[]\ntype MyClass =\n// highlight-next-line\n#if FABLE_COMPILER_JAVASCRIPT\n static member test (arg: U2) = \n match arg with\n | U2.Case1 s -> printfn "This is a string: %s" s\n | U2.Case2 i -> printfn "This is a integer: %i" i\n// highlight-next-line\n#else\n static member test(arg: int) =\n printfn "This is a integer: %i" arg\n static member test(arg: string) =\n printfn "This is a string: %s" arg\n// highlight-next-line\n#endif\n\nlet result = MyClass.test(10)\n'})}),"\n",(0,s.jsx)(n.p,{children:"This would allow us to use the erased union only in JavaScript and use the F# overloads in F#. But i have not investigated how this would work for python \ud83d\ude05."}),"\n",(0,s.jsx)(n.p,{children:"Due to the additional workload i decided to avoid using overloads in the api. Instead i tried finding the core functions and functions, which allow additional inputs with a different name:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"type sequence =\n static member note(id: string, text: string, ?notePosition: NotePosition) = //..\n static member noteSpanning(id1: string, id2, text: string, ?notePosition: NotePosition) = //..\n"})}),"\n",(0,s.jsx)(n.h3,{id:"javascript-optional-parameters",children:"JavaScript optional parameters"}),"\n",(0,s.jsx)(n.p,{children:"Using functions with multiple optional parameters is easily done in F#, C# and Python, but can get quite annoying in JavaScript:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'// tripple null ...\nrequirement.requirement("my id", null, null, null, rqMethod.test)\n'})})}),(0,s.jsx)(r.A,{value:"f",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'// easy!\nrequirement.requirement("My Id", rqMethod = rqMethod.analysis)\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'# easy!\nrequirement.requirement("My Id", rq_method = rq_method.analysis)\n'})})}),(0,s.jsx)(r.A,{value:"c",label:"C#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// easy!\nRequirement.requirement("My Id", rqMethod: rqMethod.analysis)\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["The JavaScript native approach would be using an object with only the values you want to set.\nThere is even a way to tell Fable to transpile parameters as object using the ",(0,s.jsx)(n.code,{children:"[ParamObject]"})," attribute."]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"f",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\nopen Fable.Core.JsInterop\n\n[]\ntype MyClass =\n [] // Start creating obj from params at index 1\n static member test (name: int, ?id: string, ?text: string, ?rqRisk: string, ?rqMethod: string) =\n 0\n\n\nMyClass.test(10, rqRisk = "Hello")\nMyClass.test(10)\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static test(name, { id, text, rqRisk, rqMethod }) {\n return 0;\n }\n}\n\nMyClass.test(10, {\n rqRisk: "Hello",\n});\n\nMyClass.test(10, {}); // Oh oh. Why an empty object?\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["As you can see adding ",(0,s.jsx)(n.em,{children:"no"})," optional parameters requires an empty object, as Fable checks if the value at object key xyz is null and not if the object is null.\nWithout the empty object the function would throw an error, whenever any of the optional parameters is referenced in the function. (Bad example as i just return 0)."]}),"\n",(0,s.jsx)(n.h3,{id:"member-names",children:"Member Names"}),"\n",(0,s.jsx)(n.p,{children:"Different languages have different expectations for member names. Aside from styling best practises, there are some things that are not possible in all languages."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsx)(n.p,{children:"F# typically uses PascalCase for class names and camelCase for member names.\nFor easier usage i ignore this rule and used camelCase for everything."})}),"\n",(0,s.jsx)(n.h4,{id:"f-reserved-keywords",children:"F# reserved keywords"}),"\n",(0,s.jsx)(n.p,{children:"The first issue i encountered where reserved keywords in F#."}),"\n",(0,s.jsxs)(n.p,{children:["For example ",(0,s.jsx)(n.code,{children:"classDiagram.``class``"}),". ",(0,s.jsx)(n.em,{children:'"class"'})," is a reserved keyword which is not allowed in F#.\nThe standard solution is wrapping the name in backticks. But at least for me on VisualStudio Community this resulted in issues with my auto complete.\nThis resulted in me handling this issue inconsistently. The issues i encountered were mostly in (optional) parameters, which is why i changed their names to PascalCase:"]}),"\n",(0,s.jsx)(n.p,{children:"For members i mostly stayed true to the backtick syntax."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"[]\ntype classMemberClassifier =\n // abstract is a reserved keyword\n static member Abstract = ClassMemberClassifier.Abstract\n // static is a reserved keyword\n static member Static = ClassMemberClassifier.Static\n static member custom str = ClassMemberClassifier.Custom str\n\n[]\ntype classDiagram =\n static member ``class`` (id: string, members: #seq) = \n"})}),"\n",(0,s.jsxs)(n.p,{children:["The C# wrapper used the C# best practise syntax ",(0,s.jsx)(n.code,{children:"@class"}),", which worked fine for me."]}),"\n",(0,s.jsx)(n.h4,{id:"c---member-name--enclosing-type",children:"C# - Member name = enclosing type"}),"\n",(0,s.jsxs)(n.p,{children:["I encountered this issue first for ",(0,s.jsx)(n.code,{children:"block.block"}),". In C# member names are not allowed to be the same as the enclosing type."]}),"\n",(0,s.jsxs)(n.p,{children:["As i am not a very experienced C# developer, i am still rather undecided on how to handle this issue.\nSo far I have been using ",(0,s.jsx)(n.code,{children:"Block.block"}),", as I am thinking about using PascalCase for all classes in C#. And if only to mute the warnings in VS Community."]}),"\n",(0,s.jsx)(n.p,{children:"If you have a strong opinion about this topic, please let me know! I am interested in hearing your thoughts."}),"\n",(0,s.jsx)(n.h4,{id:"transpiled-names",children:"Transpiled names"}),"\n",(0,s.jsxs)(n.p,{children:["And back to ",(0,s.jsx)(n.code,{children:"classDiagram.``class`` "}),". While JavaScript does not seem to care about this topic to much, Python does."]}),"\n",(0,s.jsxs)(n.p,{children:["JavaScript gives us the best result, the F# backtick syntax is transpiled to a simple camelCase name ",(0,s.jsx)(n.code,{children:"classDiagram.class"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Python on the other hand has ",(0,s.jsx)(n.code,{children:"class"})," also as reserved keyword. Fable transpiles it to ",(0,s.jsx)(n.code,{children:"classDiagram.class_"}),".\nWhich raises the question if i should simply apply this syntax to all cases with naming problems."]}),"\n",(0,s.jsx)(n.h3,{id:"docsnative-test-maintainance",children:"Docs/Native Test Maintainance"}),"\n",(0,s.jsxs)(n.p,{children:["The core library + C# wrapper were done rather quickly. I can also recycle my F# unit tests to check if transpilation works as expected, using ",(0,s.jsx)(n.a,{href:"https://github.com/Freymaurer/Fable.Pyxpecto",children:"Fable.Pyxpecto"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"But testing correct native accessibillity and writing docs (showcasing the test cases) was the most time consuming part and something i am not happy with."}),"\n",(0,s.jsx)(n.p,{children:"Here are some ideas on how to improve this:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Theoretically, i could use the transpiled tests for docs. But i still have to remove the Fable specific helper functions and replace them with native ones."}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.em,{children:(0,s.jsx)(n.a,{href:"https://github.com/kMutagene",children:"Kevin S."})})," had an idea, repurposing jupyter notebooks for docs and testing. To at least unify the testing and docs."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"If you have any ideas on how to improve this, please let me know!"}),"\n"]})}function m(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(p,{...e})}):p(e)}},9365:(e,n,t)=>{t.d(n,{A:()=>r});t(6540);var s=t(4164);const a={tabItem:"tabItem_Ymn6"};var i=t(4848);function r(e){let{children:n,hidden:t,className:r}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,s.A)(a.tabItem,r),hidden:t,children:n})}},1470:(e,n,t)=>{t.d(n,{A:()=>w});var s=t(6540),a=t(4164),i=t(3104),r=t(6347),l=t(205),o=t(7485),c=t(1682),h=t(9466);function d(e){return s.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function p(e){const{values:n,children:t}=e;return(0,s.useMemo)((()=>{const e=n??function(e){return d(e).map((e=>{let{props:{value:n,label:t,attributes:s,default:a}}=e;return{value:n,label:t,attributes:s,default:a}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function m(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function u(e){let{queryString:n=!1,groupId:t}=e;const a=(0,r.W6)(),i=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,o.aZ)(i),(0,s.useCallback)((e=>{if(!i)return;const n=new URLSearchParams(a.location.search);n.set(i,e),a.replace({...a.location,search:n.toString()})}),[i,a])]}function f(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,i=p(e),[r,o]=(0,s.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!m({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const s=t.find((e=>e.default))??t[0];if(!s)throw new Error("Unexpected error: 0 tabValues");return s.value}({defaultValue:n,tabValues:i}))),[c,d]=u({queryString:t,groupId:a}),[f,g]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,i]=(0,h.Dv)(t);return[a,(0,s.useCallback)((e=>{t&&i.set(e)}),[t,i])]}({groupId:a}),y=(()=>{const e=c??f;return m({value:e,tabValues:i})?e:null})();(0,l.A)((()=>{y&&o(y)}),[y]);return{selectedValue:r,selectValue:(0,s.useCallback)((e=>{if(!m({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);o(e),d(e),g(e)}),[d,g,i]),tabValues:i}}var g=t(2303);const y={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var b=t(4848);function x(e){let{className:n,block:t,selectedValue:s,selectValue:r,tabValues:l}=e;const o=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),h=e=>{const n=e.currentTarget,t=o.indexOf(n),a=l[t].value;a!==s&&(c(n),r(a))},d=e=>{let n=null;switch(e.key){case"Enter":h(e);break;case"ArrowRight":{const t=o.indexOf(e.currentTarget)+1;n=o[t]??o[0];break}case"ArrowLeft":{const t=o.indexOf(e.currentTarget)-1;n=o[t]??o[o.length-1];break}}n?.focus()};return(0,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":t},n),children:l.map((e=>{let{value:n,label:t,attributes:i}=e;return(0,b.jsx)("li",{role:"tab",tabIndex:s===n?0:-1,"aria-selected":s===n,ref:e=>o.push(e),onKeyDown:d,onClick:h,...i,className:(0,a.A)("tabs__item",y.tabItem,i?.className,{"tabs__item--active":s===n}),children:t??n},n)}))})}function j(e){let{lazy:n,children:t,selectedValue:a}=e;const i=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=i.find((e=>e.props.value===a));return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:i.map(((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==a})))})}function v(e){const n=f(e);return(0,b.jsxs)("div",{className:(0,a.A)("tabs-container",y.tabList),children:[(0,b.jsx)(x,{...e,...n}),(0,b.jsx)(j,{...e,...n})]})}function w(e){const n=(0,g.A)();return(0,b.jsx)(v,{...e,children:d(e.children)},String(n))}},8453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>l});var s=t(6540);const a={},i=s.createContext(a);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/984a9349.63231122.js b/assets/js/984a9349.63231122.js deleted file mode 100644 index b06b120..0000000 --- a/assets/js/984a9349.63231122.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[8240],{269:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>h,contentTitle:()=>o,default:()=>m,frontMatter:()=>l,metadata:()=>c,toc:()=>d});var s=t(4848),a=t(8453),i=t(1470),r=t(9365);const l={slug:"Library Design",title:"Library Design",authors:"freyk",tags:["fable","library","api","design","helper"]},o="Libary Design",c={permalink:"/Siren/blog/Library Design",editUrl:"https://github.com/Freymaurer/Siren/tree/main/docs/blog/2024-05-06-fable-library-design.mdx",source:"@site/blog/2024-05-06-fable-library-design.mdx",title:"Library Design",description:"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#.",date:"2024-05-06T00:00:00.000Z",tags:[{label:"fable",permalink:"/Siren/blog/tags/fable"},{label:"library",permalink:"/Siren/blog/tags/library"},{label:"api",permalink:"/Siren/blog/tags/api"},{label:"design",permalink:"/Siren/blog/tags/design"},{label:"helper",permalink:"/Siren/blog/tags/helper"}],readingTime:23.445,hasTruncateMarker:!1,authors:[{name:"Kevin Frey",title:"Maintainer of Siren",url:"https://github.com/Freymaurer",imageURL:"https://github.com/Freymaurer.png",key:"freyk"}],frontMatter:{slug:"Library Design",title:"Library Design",authors:"freyk",tags:["fable","library","api","design","helper"]},unlisted:!1},h={authorsImageUrls:[void 0]},d=[{value:"Idea \ud83d\udca1",id:"idea-",level:2},{value:"What is Fable?",id:"what-is-fable",level:2},{value:"Benefits",id:"benefits",level:3},{value:"API Design",id:"api-design",level:2},{value:"Using [<AttachMembers>]",id:"using-attachmembers",level:3},{value:"C# Compatibility",id:"c-compatibility",level:3},{value:"Code generator helper",id:"code-generator-helper",level:4},{value:"Maintainability",id:"maintainability",level:4},{value:"Python/JavaScript import and Index files",id:"pythonjavascript-import-and-index-files",level:3},{value:"Publish",id:"publish",level:2},{value:".NET",id:"net",level:3},{value:"Pypi",id:"pypi",level:3},{value:"JavaScript (+Types)",id:"javascript-types",level:3},{value:"Deep Dive",id:"deep-dive",level:2},{value:"Overloads",id:"overloads",level:3},{value:"Erased Unions",id:"erased-unions",level:4},{value:"JavaScript optional parameters",id:"javascript-optional-parameters",level:3},{value:"Member Names",id:"member-names",level:3},{value:"F# reserved keywords",id:"f-reserved-keywords",level:4},{value:"C# - Member name = enclosing type",id:"c---member-name--enclosing-type",level:4},{value:"Transpiled names",id:"transpiled-names",level:4},{value:"Docs/Native Test Maintainance",id:"docsnative-test-maintainance",level:3}];function p(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",input:"input",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#."}),"\n",(0,s.jsx)(n.admonition,{title:"Fable.Multiverse",type:"tip",children:(0,s.jsxs)(n.p,{children:["To make it as easy as possible to create a Fable library that publishes in multiple languages, I created a template called ",(0,s.jsx)(n.a,{href:"https://github.com/Freymaurer/Fable.Multiverse",children:"Fable.Multiverse"})," ","\ud83c\udf89"]})}),"\n",(0,s.jsx)(n.h2,{id:"idea-",children:"Idea \ud83d\udca1"}),"\n",(0,s.jsxs)(n.p,{children:["During a hackathon for our research data management consortium, we were discussing ideas for visualizing graph like structures in a way that allows easy gitlab integration and can be understood and used by any level of user. The idea we pursued was to add ",(0,s.jsx)(n.code,{children:".md"})," files with mermaid graphs for an easy overview. So why not write a domain specific language for mermaid graphs to make creation of such graphs easier and more error proof. Good idea, but what programming language should we use? In our consortium we have several groups, some using JavaScript, some Python, some (us) .NET or more specifically F#. Because i already have quite some experience using ",(0,s.jsx)(n.a,{href:"https://fable.io",children:"Fable"}),", i did the mental checklist to see if it would be a good fit for this project."]}),"\n",(0,s.jsxs)(n.ul,{className:"contains-task-list",children:["\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Does not need any dependencies."})," Mermaid graphs, are mostly YAML, so no complex syntax."]}),"\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Does not require IO interaction."})," We can simply focus on writing our mermaid graph as string and allow the user to do whatever they want with it."]}),"\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Only Fable compatible languages needed."})," We are already very happy offering such a tool in Python, JavaScript and F#."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:".. and thats it \ud83c\udf89 So we can start developing a libary with one codebase for 4 [5] languages."}),"\n",(0,s.jsx)(n.h2,{id:"what-is-fable",children:"What is Fable?"}),"\n",(0,s.jsxs)(n.p,{children:["Fable is a F# to X transpiler. It started out targeting only JavaScript, using a naming reference to the popular ",(0,s.jsx)(n.a,{href:"https://babeljs.io",children:"Babel"})," JavaScript transpiler. Now Fable aims to support multiple languages, all in different states. At the time of writing, the offical Fable docs state the following:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Language"}),(0,s.jsx)(n.th,{children:"Status"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JavaScript"}),(0,s.jsx)(n.td,{children:"Stable"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"TypeScript"}),(0,s.jsx)(n.td,{children:"Stable"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Dart"}),(0,s.jsx)(n.td,{children:"Beta"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Python"}),(0,s.jsx)(n.td,{children:"Beta"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Rust"}),(0,s.jsx)(n.td,{children:"Alpha"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"PHP"}),(0,s.jsx)(n.td,{children:"Experimental"})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"benefits",children:"Benefits"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Type Safety"}),". F# is a statically typed language, which means that the compiler can catch many errors before they even happen."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Lightweight Syntax"}),". F# has a very lightweight syntax, which makes it easy to read and write. It does not require a lot of boilerplate code and you can get right into the meat of your program."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Testing(?)"}),". Main codebase is written in F# as well as most tests. This allows us to also transpile the tests to other languages and run them there. This is a big advantage, as we can be sure that the tests are the same in all languages."]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{title:"But why the question mark behind testing?",type:"note",children:(0,s.jsx)(n.p,{children:"Because we can recycle the tests, to ensure correct functionality, but we still must test if the library can be used from all supported languages without hurdles."})}),"\n",(0,s.jsx)(n.h2,{id:"api-design",children:"API Design"}),"\n",(0,s.jsx)(n.p,{children:"To make the code look and feel as native as possible in all languages, there are some things we need to consider. But first let us have a look at fable transpiled code."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["The following code will use the ",(0,s.jsx)(n.a,{href:"https://fable.io/repl/",children:"Fable REPL"})," to transpile code for easy showcasing!"]})}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'let helloWorld = printfn "Hello World"\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { printf, toConsole } from "fable-library-js/String.js";\n\nexport const helloWorld = toConsole(printf("Hello World"));\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'from fable_library_js.string import (to_console, printf)\n\nhello_world: None = to_console(printf("Hello World"))\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"We can already notice some things:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Fable tries to transpile into native syntax, so for example snake_case in Python and camelCase in JavaScript"}),"\n",(0,s.jsxs)(n.li,{children:["Fable has some wrappers for functions which might have native equivalents. In F# ",(0,s.jsx)(n.code,{children:"printfn"})," is used to print to the console, in JavaScript ",(0,s.jsx)(n.code,{children:"console.log"})," and in Python ",(0,s.jsx)(n.code,{children:"print"}),". But Fable uses their own printf function to ensure 100% correct f# transpilation."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Next we will have a look at a class with some member functions."}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"type MyClass =\n static member add (x: int) (y: int) = x + y\n"})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n}\n\nexport function MyClass_$reflection() {\n return class_type("Test.MyClass", undefined, MyClass);\n}\n\nexport function MyClass_add(x, y) {\n return x + y;\n}\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'from fable_library_js.reflection import (TypeInfo, class_type)\n\ndef _expr0() -> TypeInfo:\n return class_type("Test.MyClass", None, MyClass)\n\n\nclass MyClass:\n ...\n\nMyClass_reflection = _expr0\n\ndef MyClass_add(x: int, y: int) -> int:\n return x + y\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Oh no, this does not look good. Fable does a thing called ",(0,s.jsx)(n.a,{href:"https://fable.io/docs/javascript/features.html#name-mangling",children:(0,s.jsx)(n.em,{children:"name mangling"})}),".\nHave a look at the offical docs for a deeper view on this topic.\nFor now its enought to know, this is done to allow overloading functions in F#."]}),"\n",(0,s.jsxs)(n.h3,{id:"using-attachmembers",children:["Using ",(0,s.jsx)(n.code,{children:"[]"})]}),"\n",(0,s.jsx)(n.p,{children:"But we can tell Fable that we know what we are doing and ignore name mangling."}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"open Fable.Core\n\n[]\ntype MyClass =\n static member add (x: int) (y: int) = x + y\n"})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static add(x, y) {\n return x + y;\n }\n}\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:"from fable_library_js.reflection import (TypeInfo, class_type)\n\nclass MyClass:\n @staticmethod\n def add(x: int, y: int) -> int:\n return x + y\n"})})})]}),"\n",(0,s.jsxs)(n.p,{children:["That looks better and allows us to do the following in all 3 languages: ",(0,s.jsx)(n.code,{children:"MyClass.add"}),".\nThis is the basic design i chose to use for most user facing api. All F#/Fable code not easily usable from other languages is hidden behind a facade like this."]}),"\n",(0,s.jsx)(n.h3,{id:"c-compatibility",children:"C# Compatibility"}),"\n",(0,s.jsxs)(n.p,{children:["Strangely enough allowing C# users the same ease of use as Python and JavaScript users is the hardest.\nThis is because C# has some issues with F# ",(0,s.jsx)(n.em,{children:"optional parameters"})," and F# ",(0,s.jsx)(n.em,{children:"tuples"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"In F# we can define a function like this:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'[]\ntype flowchart =\n static member raw (txt: string) = FlowchartElement txt\n static member id (txt: string) = FlowchartElement txt\n static member node (id: string, ?name: string) : FlowchartElement = ...\n\nflowchart.node("My id")\nflowchart.node("My id", "My name")\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Using the F# function in C# will result in an error, when you try to do ",(0,s.jsx)(n.code,{children:'flowchart.node("My id")'}),", as ",(0,s.jsx)(n.code,{children:"?name"})," is a ",(0,s.jsx)(n.code,{children:"Microsoft.FSharp.Core.FSharpOption"})," without any default information."]}),"\n",(0,s.jsx)(n.p,{children:"By creating a C# access layer we can avoid this issue for C# users:"}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["The C# extensions for optional parameters and tuples are taken from ",(0,s.jsx)(n.a,{href:"https://github.com/plotly/Plotly.NET",children:"Plotly.NET"})," with the help from my dear colleague ",(0,s.jsx)(n.a,{href:"https://github.com/kMutagene",children:"Kevin Schneider"}),"."]})}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"flow",label:"Flowchart.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public static class flowchart\n{\n public static FlowchartElement raw(string txt) => Siren.flowchart.raw(txt);\n\n public static FlowchartElement id(string txt) => Siren.flowchart.raw(txt);\n\n public static FlowchartElement node(string id, Optional name = default) =>\n Siren.flowchart.node(id, name.ToOption());\n //...\n}\n"})})}),(0,s.jsx)(r.A,{value:"fsharp",label:"OptionExtension.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'/// \n/// Helper type for handling the special way the Plotly.NET core API uses generics.\n/// In short, the problem arises because many optional parameters of Plotly.NET\'s core API are generics \n/// with a type constraint for `IConvertible`. This means that these parameters can be both value and reference types \n/// (e.g. `double` and `System.DateTime` both implement IConvertible).\n/// If we now have a optional parameter of type `T? where T: IConvertible` the compiler will not allow this \n/// without further type constrainst to eithe reference or value type.\n/// This is a problem because we want to 1. allow both, and 2. have a reliable way of determining if the value was not set \n/// because the F# API expects to be passed `Option.None` in that case.\n/// There exist other workarounds like checking if the value is default or null, but that changes valid default values actually set to null as well.\n/// \n/// \n/// The value to mark as optional\n/// Wether or not the wrapped value is valid. This is used downstream to determine wether to wrap this value into `Option.Some` (if true) or `Option.None` (if false)\npublic readonly record struct Optional(T Value, bool IsSome)\n{\n /// \n /// \n /// \n /// \n public static implicit operator Optional(T Value) => new(Value, true);\n\n}\n/// \n/// Extension methods for the `Optional` class\n/// \npublic static class OptionalExtensions\n{\n /// \n /// Converts the `Optional` value to `Some(value)` if the value is valid, or `None` if it is not.\n /// \n /// \n /// The `Optional` value to convert to a F# Option\n /// opt converted to `Option`\n static internal Microsoft.FSharp.Core.FSharpOption ToOption(this Optional opt) => opt.IsSome ? new(opt.Value) : Microsoft.FSharp.Core.FSharpOption.None;\n}\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"A similar issue arises with F# tuples:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"flow",label:"Flowchart.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"[]\ntype flowchart =\n static member stylesNode (nodeId: string, styles:#seq) = Flowchart.formatNodeStyles [nodeId] (List.ofSeq styles) |> FlowchartElement\n"})})}),(0,s.jsx)(r.A,{value:"extension",label:"TupleExtension.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"/// \n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\n/// \ninternal static class TupleExtensions\n{\n /// \n /// Converts a tuple.\n /// \n internal static Tuple ToTuple(this ValueTuple t) => Tuple.Create(t.Item1, t.Item2);\n}\n"})})}),(0,s.jsx)(r.A,{value:"fsharp",label:"Flowchart.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"/// \n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\n/// \npublic static class flowchart\n{\n public static FlowchartElement stylesNode(string nodeId, (string, string)[] styles) =>\n Siren.flowchart.stylesNode(nodeId, styles.Select(t => t.ToTuple()));\n}\n"})})})]}),"\n",(0,s.jsx)(n.h4,{id:"code-generator-helper",children:"Code generator helper"}),"\n",(0,s.jsx)(n.p,{children:"These incompatibilities are not only annoying but providing a consistently native C# experience, requires a wrapping for all apis.\nTo make this easier, i created a code generator that takes a F# file and generates the C# wrapper for it. .. Or at least 95% of it. The rest is done by hand."}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Code Generator"}),(0,s.jsx)("div",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open System.Reflection\n\n[]\nlet FSharpOptionDefault = "Optional"\n\nlet transformParameterTypeName (paramTypeName: string)=\n match paramTypeName with\n | "String" -> "string"\n | "Int32" -> "int"\n | "Double" -> "double"\n | "FSharpOption`1" -> FSharpOptionDefault // this is not always true but a good approximation\n | "Tuple`2" -> "(string,string)" // this is not always true but a good approximation\n | "Boolean" -> "bool"\n | _ -> paramTypeName\n\ntype ParameterInfo = {\n Type: string\n Name: string\n} with\n member this.FSharpParam =\n match this with\n | {Type = FSharpOptionDefault} -> this.Name + ".ToOption()"\n | _ -> this.Name\n member this.CSharpParam =\n match this with\n | {Type = FSharpOptionDefault} -> sprintf "%s %s = default" this.Type this.Name\n | _ -> sprintf "%s %s" this.Type this.Name\n\n static member create(typeName: string, name: string) =\n {Type = transformParameterTypeName typeName; Name = name}\n\nlet generateCSharpCode<\'A>() =\n\n let t = typeof<\'A>\n let members = t.GetMethods(BindingFlags.Static ||| BindingFlags.Public)\n\n let mutable csharpCode = sprintf "public static class %s\\n{\\n" t.Name\n for m in members do\n let methodName = \n let name0 = m.Name\n if name0.StartsWith("get_") then\n name0.Substring(4)\n else\n name0\n let returnType = m.ReturnType.Name\n let params0 = \n m.GetParameters() \n |> Array.map (fun p -> ParameterInfo.create(p.ParameterType.Name, p.Name))\n let csharpParameters =\n if params0.Length = 0 then\n ""\n else \n params0\n |> Array.map _.CSharpParam\n |> String.concat(", ")\n |> fun s -> "(" + s + ")"\n \n let fsharpParameters = \n if params0.Length = 0 then\n ""\n else \n params0 \n |> Array.map _.FSharpParam\n |> String.concat(", ")\n |> fun s -> "(" + s + ")"\n \n let methodSignature = $"public static {transformParameterTypeName returnType} {methodName}{csharpParameters}"\n let methodBody = \n if methodName.StartsWith("get_") then\n let withoutGet = methodName.Substring(4)\n $"return Siren.{t.Name}.{withoutGet};"\n else\n $" => Siren.{t.Name}.{methodName}{fsharpParameters};"\n csharpCode <- csharpCode + $" {methodSignature}\\n {methodBody}\\n"\n\n csharpCode <- csharpCode + "}\\n"\n csharpCode\n\nlet test() = \n generateCSharpCode() // Here you can pass any type you want to generate C# code for\n |> printfn "%A"\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"This is something i did not want to spend a lot of time on, so when i quickly wrote this and noticed it was able to create most of the C# code correctly.\nI think improving this code to create everything perfectly would be awesome, but would have taken me longer thant fixing the few mistakes it makes by hand."}),"\n",(0,s.jsx)(n.h4,{id:"maintainability",children:"Maintainability"}),"\n",(0,s.jsx)(n.p,{children:"This is a big issue. Whenever i update my f# api i must also update the c# wrapper.\nChanges are mostly catched by the compiler but missings functions are not."}),"\n",(0,s.jsx)(n.p,{children:"That is why i added unit tests to check the count and name of a c# and f# class and compare it for equality:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public static class Utils\n{\n public static int GetMemberCount(Type type)\n {\n var members = type.GetMembers();\n return members.Length;\n }\n\n public static List GetMemberNameDifferences(Type type1, Type type2)\n {\n List differences = new List();\n //transform string to lower\n var type1Members = type1.GetMembers().Select(m => m.Name.ToLower());\n var type2Members = type2.GetMembers().Select(m => m.Name.ToLower());\n differences.AddRange(type1Members.Except(type2Members));\n differences.AddRange(type2Members.Except(type1Members));\n\n return differences;\n }\n\n public static void CompareClasses(Type csharpType, Type fsharpType)\n {\n int csharpMemberCount = GetMemberCount(csharpType);\n int fsharpMemberCount = GetMemberCount(fsharpType);\n List differences = GetMemberNameDifferences(fsharpType, csharpType);\n\n Assert.Empty(differences);\n Assert.Equal(fsharpMemberCount, csharpMemberCount);\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"This at least helps me to catch missing functions in the c# wrapper, even if i still have to write them by hand."}),"\n",(0,s.jsx)(n.h3,{id:"pythonjavascript-import-and-index-files",children:"Python/JavaScript import and Index files"}),"\n",(0,s.jsxs)(n.p,{children:["F# and C# use namespaces to organize code. I can have multiple files with the same namespace and access all functions simply by writing ",(0,s.jsx)(n.code,{children:"open Siren"}),"/",(0,s.jsx)(n.code,{children:"using Siren.Sea;"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"In Python and JavaScript this is not possible. Here imports happen on a file basis. So i needed to have a single file that imports all other files and exports them."}),"\n",(0,s.jsx)(n.p,{children:"Luckily this file can be created automatically!"}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Index File"}),(0,s.jsx)("div",{children:(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"util",label:"Util.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.Util\n\nopen System\nopen System.IO\nopen System.Text.RegularExpressions\n\ntype FileInformation = {\n FilePath : string\n Lines : string []\n} with\n static member create(filePath: string, lines: string []) = {\n FilePath = filePath\n Lines = lines\n }\n\nlet getAllFiles(path: string, extension: string) = \n let options = EnumerationOptions()\n options.RecurseSubdirectories <- true\n IO.Directory.EnumerateFiles(path,extension,options)\n |> Seq.filter (fun s -> s.Contains("fable_modules") |> not)\n |> Array.ofSeq\n\nlet findClasses (rootPath: string) (cois: string []) (regexPattern: string -> string) (filePaths: seq) = \n let files = [|\n for fp in filePaths do\n yield FileInformation.create(fp, System.IO.File.ReadAllLines (fp))\n |]\n let importStatements = ResizeArray()\n let findClass (className: string) = \n /// maybe set this as default if you do not want any whitelist\n let classNameDefault = @"[a-zA-Z_0-9]"\n let pattern = Regex.Escape(className) |> regexPattern\n let regex = Regex(pattern)\n let mutable found = false\n let mutable result = None\n let enum = files.GetEnumerator()\n while not found && enum.MoveNext() do\n let fileInfo = enum.Current :?> FileInformation\n for line in fileInfo.Lines do\n let m = regex.Match(line)\n match m.Success with\n | true -> \n found <- true\n result <- Some <| (className, IO.Path.GetRelativePath(rootPath,fileInfo.FilePath))\n | false ->\n ()\n match result with\n | None ->\n failwithf "Unable to find %s" className\n | Some r ->\n importStatements.Add r\n for coi in cois do findClass coi\n importStatements\n |> Array.ofSeq\n\nlet writeIndexFile (path: string) (fileName: string) (content: string) =\n let filePath = Path.Combine(path, fileName)\n File.WriteAllText(filePath, content)\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"Indexjs.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.JS\n\nlet private getAllJsFiles (path: string) fileExtension = \n Util.getAllFiles(path,$"*.{fileExtension}")\n\nlet private pattern (className: string) = sprintf @"^export class (?%s)+[\\s{].*({)?" className\n\nlet private findJsClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \n Util.findClasses rootPath whiteList pattern filePaths\n\nopen System.Text\n\nlet private createImportStatements (info: (string*string) []) =\n let sb = StringBuilder()\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\n for filePath, imports in importCollection do\n let p = filePath.Replace("\\\\","/").Replace("ts","js")\n sb.Append "export { " |> ignore\n sb.AppendJoin(", ", imports) |> ignore\n sb.Append " } from " |> ignore\n sb.Append (sprintf "\\"./%s\\"" p) |> ignore\n sb.Append ";" |> ignore\n sb.AppendLine() |> ignore\n sb.ToString()\n\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string [], fileExtension: string) =\n getAllJsFiles rootPath fileExtension\n |> findJsClasses rootPath whiteList\n |> createImportStatements\n |> Util.writeIndexFile rootPath fileName\n\nlet generate(rootPath: string) (ts: bool) = \n let extension = if ts then "ts" else "js"\n generateIndexfile(rootPath, $"index.{extension}", WhiteList.WhiteList, extension)\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Indexpy.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'let private getAllJsFiles(path: string) = \n Util.getAllFiles(path,"*.py")\n\nlet private pattern (className: string) = sprintf @"^class (?%s)+(\\(|:).*$" className\n\nlet private findPyClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \n Util.findClasses rootPath whiteList pattern filePaths\n\nopen System.Text\n\nlet private createImportStatements (info: (string*string) []) =\n let sb = StringBuilder()\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\n for filePath, imports in importCollection do\n let p = filePath |> Path.GetFileNameWithoutExtension\n sb.Append (sprintf "from .%s import " p) |> ignore\n sb.AppendJoin(", ", imports) |> ignore\n sb.AppendLine() |> ignore\n sb.ToString()\n\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string []) =\n getAllJsFiles(rootPath)\n |> findPyClasses rootPath whiteList\n |> createImportStatements\n |> Util.writeIndexFile rootPath fileName\n\nlet generate(rootPath: string) (name: string) = \n // code to make camelcase to snakecase\n /// This is because we currently snake_case everything that does not start with a capital letter\n let camelCaseToSnakeCase (str: string) = \n if Char.IsUpper str.[0] then\n str \n else\n str \n |> Seq.fold (fun (acc: string) c -> \n if Char.IsUpper c then \n acc + "_" + string (Char.ToLower c) \n else \n acc + string c\n ) ""\n let snake_case_white_list = WhiteList.WhiteList |> Array.map camelCaseToSnakeCase\n generateIndexfile(rootPath, name, snake_case_white_list)\n'})})}),(0,s.jsx)(r.A,{value:"whitelist",label:"WhiteList.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.WhiteList\n\nlet WhiteList = [|\n "SirenElement"\n // ThemeVaruiables\n "themeVariable"\n "quadrantTheme"; \n "gitTheme"; \n "timelineTheme"; \n "xyChartTheme"; \n "pieTheme";\n // Config\n "graphConfig";\n "flowchartConfig"; \n "sequenceConfig"; \n "ganttConfig"; \n "journeyConfig"; \n "timelineConfig"; \n "classConfig"; \n "stateConfig"; \n "erConfig"; \n "quadrantChartConfig"; \n "pieConfig"; \n "sankeyConfig"; \n "xyChartConfig"; \n "mindmapConfig"; \n "gitGraphConfig"; \n "requirementConfig"; \n // Graphs and Helpers\n "formatting"; \n "direction"; \n "flowchart"; \n "notePosition"; \n "sequence"; \n "classMemberVisibility"; \n "classMemberClassifier"; \n "classDirection"; \n "classCardinality"; \n "classRltsType"; \n "classDiagram"; \n "stateDiagram"; \n "erKey"; \n "erCardinality"; \n "erDiagram"; \n "journey"; \n "ganttTime"; \n "ganttTags"; \n "ganttUnit"; \n "gantt"; \n "pieChart"; \n "quadrant"; \n "rqRisk"; \n "rqMethod"; \n "requirement"; \n "gitType"; \n "git"; \n "mindmap"; \n "timeline"; \n "sankey"; \n "xyChart"; \n "block"; \n "theme"; \n "siren"; \n|]\n'})})})]})})]}),"\n",(0,s.jsx)(n.p,{children:"The resulting files look like this:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"js",label:"index.js",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'export { SirenElement } from "./SirenTypes.js";\nexport { themeVariable, quadrantTheme, gitTheme, timelineTheme, xyChartTheme, pieTheme } from "./ThemeVariables.js";\nexport { graphConfig, flowchartConfig, sequenceConfig, ganttConfig, journeyConfig, timelineConfig, classConfig, stateConfig, erConfig, quadrantChartConfig, pieConfig, sankeyConfig, xyChartConfig, mindmapConfig, gitGraphConfig, requirementConfig } from "./Config.js";\nexport { formatting, direction, flowchart, notePosition, sequence, classMemberVisibility, classMemberClassifier, classDirection, classCardinality, classRltsType, classDiagram, stateDiagram, erKey, erCardinality, erDiagram, journey, ganttTime, ganttTags, ganttUnit, gantt, pieChart, quadrant, rqRisk, rqMethod, requirement, gitType, git, mindmap, timeline, sankey, xyChart, block, theme, siren } from "./Siren.js";\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"index.py/__init__.py",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-py",children:"from .siren_types import SirenElement\nfrom .theme_variables import theme_variable, quadrant_theme, git_theme, timeline_theme, xy_chart_theme, pie_theme\nfrom .config import graph_config, flowchart_config, sequence_config, gantt_config, journey_config, timeline_config, class_config, state_config, er_config, quadrant_chart_config, pie_config, sankey_config, xy_chart_config, mindmap_config, git_graph_config, requirement_config\nfrom .siren import formatting, direction, flowchart, note_position, sequence, class_member_visibility, class_member_classifier, class_direction, class_cardinality, class_rlts_type, class_diagram, state_diagram, er_key, er_cardinality, er_diagram, journey, gantt_time, gantt_tags, gantt_unit, gantt, pie_chart, quadrant, rq_risk, rq_method, requirement, git_type, git, mindmap, timeline, sankey, xy_chart, block, theme, siren\n"})})})]}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["Python packages will default import whatever is in the ",(0,s.jsx)(n.code,{children:"__init__.py"})," file. So we must simply name the index file for python as such (at least for publishing)."]})}),"\n",(0,s.jsx)(n.h2,{id:"publish",children:"Publish"}),"\n",(0,s.jsx)(n.p,{children:"As soon as we get to the publishing step everything is back to standard for the respective languages."}),"\n",(0,s.jsx)(n.h3,{id:"net",children:".NET"}),"\n",(0,s.jsxs)(n.p,{children:["The easiest. We already have the required project files, no need to transpile. So we can simply use ",(0,s.jsx)(n.code,{children:"dotnet pack"})," to create .nupkg files.\nThen ",(0,s.jsx)(n.code,{children:"dotnet nuget push"})," to push to ",(0,s.jsx)(n.a,{href:"https://www.nuget.org",children:"nuget.org"}),"."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h3,{id:"pypi",children:"Pypi"}),"\n",(0,s.jsxs)(n.p,{children:["Also quite easy. Transpile f# code to python code, copy ",(0,s.jsx)(n.code,{children:"pyproject.toml"})," and ",(0,s.jsx)(n.code,{children:"README.md"})," into the ",(0,s.jsx)(n.code,{children:"dist"})," folder and create ",(0,s.jsx)(n.code,{children:"index.py"})," file.\nRun ",(0,s.jsx)(n.code,{children:"python -m poetry build"})," to create a publishing files. Then publish files with ",(0,s.jsx)(n.code,{children:"python -m poetry publish"})]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h3,{id:"javascript-types",children:"JavaScript (+Types)"}),"\n",(0,s.jsxs)(n.p,{children:["For this i followed the Example of ",(0,s.jsx)(n.a,{href:"https://fable.io/blog/2023/2023-04-20-Better_Typed_than_Sorry.html",children:"Better Typed than Sorry"})," by ",(0,s.jsx)(n.a,{href:"https://github.com/alfonsogarciacaro",children:"Alfonso Garc\xeda-Caro"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Transpile F# to TypeScript, then use ",(0,s.jsx)(n.code,{children:"tsc"})," to transpile TypeScript to JavaScript and type information files (",(0,s.jsx)(n.code,{children:"*.d.ts"}),"). Add the ",(0,s.jsx)(n.code,{children:"index.js"})," file to the ",(0,s.jsx)(n.code,{children:"dist folder"}),"\nThen publish to npm with ",(0,s.jsx)(n.code,{children:"npm publish"}),"."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h2,{id:"deep-dive",children:"Deep Dive"}),"\n",(0,s.jsx)(n.p,{children:"From here on are some additional issues i encountered during development."}),"\n",(0,s.jsx)(n.h3,{id:"overloads",children:"Overloads"}),"\n",(0,s.jsxs)(n.p,{children:["The concept of allowing different inputs for the same function exists in f#, as well as python and javascript.\nBy using the ",(0,s.jsx)(n.code,{children:"[]"})," attribute we are not longer allowed to use standard f# overloads.\nBecause JavaScript does not have the same kind of type interference it is unable to recognice which function should be invoked:"]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\n\n[]\ntype MyClass =\n static member add (x: int, y: int) = x * y // this should be invoked\n static member add (x: string, y: string) = x + y\n\nlet result = MyClass.add(10, 20)\n\nprintfn "%A" result\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\nimport { printf, toConsole } from "fable-library-js/String.js";\n\nexport class MyClass {\n constructor() {\n }\n static add(x, y) {\n return x * y;\n }\n static add(x, y) { // This shadows the function above and is invoked\n return x + y;\n }\n}\n\nexport const result = MyClass.add(10, 20); // This will return 30\n\ntoConsole(printf("%A"))(result);\n\n'})})})]}),"\n",(0,s.jsx)(n.h4,{id:"erased-unions",children:"Erased Unions"}),"\n",(0,s.jsxs)(n.p,{children:["It is possible to imitate js overload behaviour by using ",(0,s.jsx)(n.a,{href:"https://fable.io/docs/javascript/features.html#erased-unions",children:"erased unions"}),".\nHere we use a Fable provides discriminate union called ",(0,s.jsx)(n.code,{children:"U2"}),"(",(0,s.jsx)(n.code,{children:"U3"}),", ",(0,s.jsx)(n.code,{children:"U4"}),"...). After transpilation it is replaced by a js type check."]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\nopen Fable.Core.JsInterop\n\n[]\ntype MyClass =\n static member test (arg: U2) = \n match arg with\n | U2.Case1 s -> printfn "This is a string: %s" s\n | U2.Case2 i -> printfn "This is a integer: %i" i\n\nlet result = MyClass.test(U2.Case2 10) // or MyClass.test(!^10)\n\nprintfn "%A" result\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { printf, toConsole } from "fable-library-js/String.js";\nimport { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static test(arg) {\n if (typeof arg === "number") {\n const i = arg;\n toConsole(printf("This is a integer: %i"))(i);\n }\n else {\n const s = arg;\n toConsole(printf("This is a string: %s"))(s);\n }\n }\n}\n\nexport const result = MyClass.test(10);\n\ntoConsole(printf("%A"))(); // This is a integer: 10\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"As you can see, this looks really nice in JavaScript, but is cumbersome to use in F#."}),"\n",(0,s.jsx)(n.p,{children:"It would be possible to do do both and use f# overloads and shadow them with the erased union. But this would add more additional maintainance work."}),"\n",(0,s.jsxs)(n.admonition,{title:"Compiler Statements",type:"info",children:[(0,s.jsxs)(n.p,{children:["We can use ",(0,s.jsx)(n.code,{children:"#if FABLE_COMPILER ... #else ... #endif"})," syntax to include code only in certain compiler states."]}),(0,s.jsx)(n.p,{children:"In the following example the erased union is only used (and accessible!) when the code is transpiled by Fable to JavaScript."})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'[]\ntype MyClass =\n// highlight-next-line\n#if FABLE_COMPILER_JAVASCRIPT\n static member test (arg: U2) = \n match arg with\n | U2.Case1 s -> printfn "This is a string: %s" s\n | U2.Case2 i -> printfn "This is a integer: %i" i\n// highlight-next-line\n#else\n static member test(arg: int) =\n printfn "This is a integer: %i" arg\n static member test(arg: string) =\n printfn "This is a string: %s" arg\n// highlight-next-line\n#endif\n\nlet result = MyClass.test(10)\n'})}),"\n",(0,s.jsx)(n.p,{children:"This would allow us to use the erased union only in JavaScript and use the F# overloads in F#. But i have not investigated how this would work for python \ud83d\ude05."}),"\n",(0,s.jsx)(n.p,{children:"Due to the additional workload i decided to avoid using overloads in the api. Instead i tried finding the core functions and functions, which allow additional inputs with a different name:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"type sequence =\n static member note(id: string, text: string, ?notePosition: NotePosition) = //..\n static member noteSpanning(id1: string, id2, text: string, ?notePosition: NotePosition) = //..\n"})}),"\n",(0,s.jsx)(n.h3,{id:"javascript-optional-parameters",children:"JavaScript optional parameters"}),"\n",(0,s.jsx)(n.p,{children:"Using functions with multiple optional parameters is easily done in F#, C# and Python, but can get quite annoying in JavaScript:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'// tripple null ...\nrequirement.requirement("my id", null, null, null, rqMethod.test)\n'})})}),(0,s.jsx)(r.A,{value:"f",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'// easy!\nrequirement.requirement("My Id", rqMethod = rqMethod.analysis)\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'# easy!\nrequirement.requirement("My Id", rq_method = rq_method.analysis)\n'})})}),(0,s.jsx)(r.A,{value:"c",label:"C#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// easy!\nRequirement.requirement("My Id", rqMethod: rqMethod.analysis)\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["The JavaScript native approach would be using an object with only the values you want to set.\nThere is even a way to tell Fable to transpile parameters as object using the ",(0,s.jsx)(n.code,{children:"[ParamObject]"})," attribute."]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"f",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\nopen Fable.Core.JsInterop\n\n[]\ntype MyClass =\n [] // Start creating obj from params at index 1\n static member test (name: int, ?id: string, ?text: string, ?rqRisk: string, ?rqMethod: string) =\n 0\n\n\nMyClass.test(10, rqRisk = "Hello")\nMyClass.test(10)\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static test(name, { id, text, rqRisk, rqMethod }) {\n return 0;\n }\n}\n\nMyClass.test(10, {\n rqRisk: "Hello",\n});\n\nMyClass.test(10, {}); // Oh oh. Why an empty object?\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["As you can see adding ",(0,s.jsx)(n.em,{children:"no"})," optional parameters requires an empty object, as Fable checks if the value at object key xyz is null and not if the object is null.\nWithout the empty object the function would throw an error, whenever any of the optional parameters is referenced in the function. (Bad example as i just return 0)."]}),"\n",(0,s.jsx)(n.h3,{id:"member-names",children:"Member Names"}),"\n",(0,s.jsx)(n.p,{children:"Different languages have different expectations for member names. Aside from styling best practises, there are some things that are not possible in all languages."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsx)(n.p,{children:"F# typically uses PascalCase for class names and camelCase for member names.\nFor easier usage i ignore this rule and used camelCase for everything."})}),"\n",(0,s.jsx)(n.h4,{id:"f-reserved-keywords",children:"F# reserved keywords"}),"\n",(0,s.jsx)(n.p,{children:"The first issue i encountered where reserved keywords in F#."}),"\n",(0,s.jsxs)(n.p,{children:["For example ",(0,s.jsx)(n.code,{children:"classDiagram.``class``"}),". ",(0,s.jsx)(n.em,{children:'"class"'})," is a reserved keyword which is not allowed in F#.\nThe standard solution is wrapping the name in backticks. But at least for me on VisualStudio Community this resulted in issues with my auto complete.\nThis resulted in me handling this issue inconsistently. The issues i encountered were mostly in (optional) parameters, which is why i changed their names to PascalCase:"]}),"\n",(0,s.jsx)(n.p,{children:"For members i mostly stayed true to the backtick syntax."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"[]\ntype classMemberClassifier =\n // abstract is a reserved keyword\n static member Abstract = ClassMemberClassifier.Abstract\n // static is a reserved keyword\n static member Static = ClassMemberClassifier.Static\n static member custom str = ClassMemberClassifier.Custom str\n\n[]\ntype classDiagram =\n static member ``class`` (id: string, members: #seq) = \n"})}),"\n",(0,s.jsxs)(n.p,{children:["The C# wrapper used the C# best practise syntax ",(0,s.jsx)(n.code,{children:"@class"}),", which worked fine for me."]}),"\n",(0,s.jsx)(n.h4,{id:"c---member-name--enclosing-type",children:"C# - Member name = enclosing type"}),"\n",(0,s.jsxs)(n.p,{children:["I encountered this issue first for ",(0,s.jsx)(n.code,{children:"block.block"}),". In C# member names are not allowed to be the same as the enclosing type."]}),"\n",(0,s.jsxs)(n.p,{children:["As i am not a very experienced C# developer, i am still rather undecided on how to handle this issue.\nSo far I have been using ",(0,s.jsx)(n.code,{children:"Block.block"}),", as I am thinking about using PascalCase for all classes in C#. And if only to mute the warnings in VS Community."]}),"\n",(0,s.jsx)(n.p,{children:"If you have a strong opinion about this topic, please let me know! I am interested in hearing your thoughts."}),"\n",(0,s.jsx)(n.h4,{id:"transpiled-names",children:"Transpiled names"}),"\n",(0,s.jsxs)(n.p,{children:["And back to ",(0,s.jsx)(n.code,{children:"classDiagram.``class`` "}),". While JavaScript does not seem to care about this topic to much, Python does."]}),"\n",(0,s.jsxs)(n.p,{children:["JavaScript gives us the best result, the F# backtick syntax is transpiled to a simple camelCase name ",(0,s.jsx)(n.code,{children:"classDiagram.class"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Python on the other hand has ",(0,s.jsx)(n.code,{children:"class"})," also as reserved keyword. Fable transpiles it to ",(0,s.jsx)(n.code,{children:"classDiagram.class_"}),".\nWhich raises the question if i should simply apply this syntax to all cases with naming problems."]}),"\n",(0,s.jsx)(n.h3,{id:"docsnative-test-maintainance",children:"Docs/Native Test Maintainance"}),"\n",(0,s.jsxs)(n.p,{children:["The core library + C# wrapper were done rather quickly. I can also recycle my F# unit tests to check if transpilation works as expected, using ",(0,s.jsx)(n.a,{href:"https://github.com/Freymaurer/Fable.Pyxpecto",children:"Fable.Pyxpecto"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"But testing correct native accessibillity and writing docs (showcasing the test cases) was the most time consuming part and something i am not happy with."}),"\n",(0,s.jsx)(n.p,{children:"Here are some ideas on how to improve this:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Theoretically, i could use the transpiled tests for docs. But i still have to remove the Fable specific helper functions and replace them with native ones."}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.em,{children:(0,s.jsx)(n.a,{href:"https://github.com/kMutagene",children:"Kevin S."})})," had an idea, repurposing jupyter notebooks for docs and testing. To at least unify the testing and docs."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"If you have any ideas on how to improve this, please let me know!"}),"\n"]})}function m(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(p,{...e})}):p(e)}},9365:(e,n,t)=>{t.d(n,{A:()=>r});t(6540);var s=t(4164);const a={tabItem:"tabItem_Ymn6"};var i=t(4848);function r(e){let{children:n,hidden:t,className:r}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,s.A)(a.tabItem,r),hidden:t,children:n})}},1470:(e,n,t)=>{t.d(n,{A:()=>w});var s=t(6540),a=t(4164),i=t(3104),r=t(6347),l=t(205),o=t(7485),c=t(1682),h=t(9466);function d(e){return s.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function p(e){const{values:n,children:t}=e;return(0,s.useMemo)((()=>{const e=n??function(e){return d(e).map((e=>{let{props:{value:n,label:t,attributes:s,default:a}}=e;return{value:n,label:t,attributes:s,default:a}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function m(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function u(e){let{queryString:n=!1,groupId:t}=e;const a=(0,r.W6)(),i=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,o.aZ)(i),(0,s.useCallback)((e=>{if(!i)return;const n=new URLSearchParams(a.location.search);n.set(i,e),a.replace({...a.location,search:n.toString()})}),[i,a])]}function g(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,i=p(e),[r,o]=(0,s.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!m({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const s=t.find((e=>e.default))??t[0];if(!s)throw new Error("Unexpected error: 0 tabValues");return s.value}({defaultValue:n,tabValues:i}))),[c,d]=u({queryString:t,groupId:a}),[g,f]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,i]=(0,h.Dv)(t);return[a,(0,s.useCallback)((e=>{t&&i.set(e)}),[t,i])]}({groupId:a}),y=(()=>{const e=c??g;return m({value:e,tabValues:i})?e:null})();(0,l.A)((()=>{y&&o(y)}),[y]);return{selectedValue:r,selectValue:(0,s.useCallback)((e=>{if(!m({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);o(e),d(e),f(e)}),[d,f,i]),tabValues:i}}var f=t(2303);const y={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var b=t(4848);function x(e){let{className:n,block:t,selectedValue:s,selectValue:r,tabValues:l}=e;const o=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),h=e=>{const n=e.currentTarget,t=o.indexOf(n),a=l[t].value;a!==s&&(c(n),r(a))},d=e=>{let n=null;switch(e.key){case"Enter":h(e);break;case"ArrowRight":{const t=o.indexOf(e.currentTarget)+1;n=o[t]??o[0];break}case"ArrowLeft":{const t=o.indexOf(e.currentTarget)-1;n=o[t]??o[o.length-1];break}}n?.focus()};return(0,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":t},n),children:l.map((e=>{let{value:n,label:t,attributes:i}=e;return(0,b.jsx)("li",{role:"tab",tabIndex:s===n?0:-1,"aria-selected":s===n,ref:e=>o.push(e),onKeyDown:d,onClick:h,...i,className:(0,a.A)("tabs__item",y.tabItem,i?.className,{"tabs__item--active":s===n}),children:t??n},n)}))})}function j(e){let{lazy:n,children:t,selectedValue:a}=e;const i=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=i.find((e=>e.props.value===a));return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:i.map(((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==a})))})}function v(e){const n=g(e);return(0,b.jsxs)("div",{className:(0,a.A)("tabs-container",y.tabList),children:[(0,b.jsx)(x,{...e,...n}),(0,b.jsx)(j,{...e,...n})]})}function w(e){const n=(0,f.A)();return(0,b.jsx)(v,{...e,children:d(e.children)},String(n))}},8453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>l});var s=t(6540);const a={},i=s.createContext(a);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/984a9349.ac019b8e.js b/assets/js/984a9349.ac019b8e.js new file mode 100644 index 0000000..46bd809 --- /dev/null +++ b/assets/js/984a9349.ac019b8e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[8240],{269:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>h,contentTitle:()=>o,default:()=>m,frontMatter:()=>l,metadata:()=>c,toc:()=>d});var s=t(4848),a=t(8453),i=t(1470),r=t(9365);const l={slug:"Library Design",title:"Library Design",authors:"freyk",tags:["fable","library","api","design","helper"]},o="Libary Design",c={permalink:"/Siren/blog/Library Design",editUrl:"https://github.com/Freymaurer/Siren/tree/main/docs/blog/2024-05-06-fable-library-design.mdx",source:"@site/blog/2024-05-06-fable-library-design.mdx",title:"Library Design",description:"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#.",date:"2024-05-06T00:00:00.000Z",tags:[{label:"fable",permalink:"/Siren/blog/tags/fable"},{label:"library",permalink:"/Siren/blog/tags/library"},{label:"api",permalink:"/Siren/blog/tags/api"},{label:"design",permalink:"/Siren/blog/tags/design"},{label:"helper",permalink:"/Siren/blog/tags/helper"}],readingTime:23.63,hasTruncateMarker:!1,authors:[{name:"Kevin Frey",title:"Maintainer of Siren",url:"https://github.com/Freymaurer",imageURL:"https://github.com/Freymaurer.png",key:"freyk"}],frontMatter:{slug:"Library Design",title:"Library Design",authors:"freyk",tags:["fable","library","api","design","helper"]},unlisted:!1},h={authorsImageUrls:[void 0]},d=[{value:"Idea \ud83d\udca1",id:"idea-",level:2},{value:"What is Fable?",id:"what-is-fable",level:2},{value:"Benefits",id:"benefits",level:3},{value:"API Design",id:"api-design",level:2},{value:"Using [<AttachMembers>]",id:"using-attachmembers",level:3},{value:"C# Compatibility",id:"c-compatibility",level:3},{value:"Code generator helper",id:"code-generator-helper",level:4},{value:"Maintainability",id:"maintainability",level:4},{value:"Python/JavaScript import and Index files",id:"pythonjavascript-import-and-index-files",level:3},{value:"Publish",id:"publish",level:2},{value:".NET",id:"net",level:3},{value:"Pypi",id:"pypi",level:3},{value:"JavaScript (+Types)",id:"javascript-types",level:3},{value:"Deep Dive",id:"deep-dive",level:2},{value:"Overloads",id:"overloads",level:3},{value:"Erased Unions",id:"erased-unions",level:4},{value:"JavaScript optional parameters",id:"javascript-optional-parameters",level:3},{value:"Member Names",id:"member-names",level:3},{value:"F# reserved keywords",id:"f-reserved-keywords",level:4},{value:"C# - Member name = enclosing type",id:"c---member-name--enclosing-type",level:4},{value:"Transpiled names",id:"transpiled-names",level:4},{value:"Docs/Native Test Maintainance",id:"docsnative-test-maintainance",level:3}];function p(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",input:"input",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Siren consists of one main code base written in F#. This code base is then transpiled to JavaScript, TypeScript, Python and made accessible from C#."}),"\n",(0,s.jsx)(n.admonition,{title:"Fable.Multiverse",type:"tip",children:(0,s.jsxs)(n.p,{children:["To make it as easy as possible to create a Fable library that publishes in multiple languages, I created a template called ",(0,s.jsx)(n.a,{href:"https://github.com/Freymaurer/Fable.Multiverse",children:"Fable.Multiverse"})," ","\ud83c\udf89"]})}),"\n",(0,s.jsx)(n.h2,{id:"idea-",children:"Idea \ud83d\udca1"}),"\n",(0,s.jsxs)(n.p,{children:["During a hackathon for our research data management consortium, we were discussing ideas for visualizing graph like structures in a way that allows easy gitlab integration and can be understood and used by any level of user. The idea we pursued was to add ",(0,s.jsx)(n.code,{children:".md"})," files with mermaid graphs for an easy overview. So why not write a domain specific language for mermaid graphs to make creation of such graphs easier and more error proof. Good idea, but what programming language should we use? In our consortium we have several groups, some using JavaScript, some Python, some (us) .NET or more specifically F#. Because i already have quite some experience using ",(0,s.jsx)(n.a,{href:"https://fable.io",children:"Fable"}),", i did the mental checklist to see if it would be a good fit for this project."]}),"\n",(0,s.jsxs)(n.ul,{className:"contains-task-list",children:["\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Does not need any dependencies."})," Mermaid graphs, are mostly YAML, so no complex syntax."]}),"\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Does not require IO interaction."})," We can simply focus on writing our mermaid graph as string and allow the user to do whatever they want with it."]}),"\n",(0,s.jsxs)(n.li,{className:"task-list-item",children:[(0,s.jsx)(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",(0,s.jsx)(n.em,{children:"Only Fable compatible languages needed."})," We are already very happy offering such a tool in Python, JavaScript and F#."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:".. and thats it \ud83c\udf89 So we can start developing a libary with one codebase for 4 [5] languages."}),"\n",(0,s.jsx)(n.h2,{id:"what-is-fable",children:"What is Fable?"}),"\n",(0,s.jsxs)(n.p,{children:["Fable is a F# to X transpiler. It started out targeting only JavaScript, using a naming reference to the popular ",(0,s.jsx)(n.a,{href:"https://babeljs.io",children:"Babel"})," JavaScript transpiler. Now Fable aims to support multiple languages, all in different states. At the time of writing, the offical Fable docs state the following:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Language"}),(0,s.jsx)(n.th,{children:"Status"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JavaScript"}),(0,s.jsx)(n.td,{children:"Stable"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"TypeScript"}),(0,s.jsx)(n.td,{children:"Stable"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Dart"}),(0,s.jsx)(n.td,{children:"Beta"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Python"}),(0,s.jsx)(n.td,{children:"Beta"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Rust"}),(0,s.jsx)(n.td,{children:"Alpha"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"PHP"}),(0,s.jsx)(n.td,{children:"Experimental"})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"benefits",children:"Benefits"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Type Safety"}),". F# is a statically typed language, which means that the compiler can catch many errors before they even happen."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Lightweight Syntax"}),". F# has a very lightweight syntax, which makes it easy to read and write. It does not require a lot of boilerplate code and you can get right into the meat of your program."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Testing(?)"}),". Main codebase is written in F# as well as most tests. This allows us to also transpile the tests to other languages and run them there. This is a big advantage, as we can be sure that the tests are the same in all languages."]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{title:"But why the question mark behind testing?",type:"note",children:(0,s.jsx)(n.p,{children:"Because we can recycle the tests, to ensure correct functionality, but we still must test if the library can be used from all supported languages without hurdles."})}),"\n",(0,s.jsx)(n.h2,{id:"api-design",children:"API Design"}),"\n",(0,s.jsx)(n.p,{children:"To make the code look and feel as native as possible in all languages, there are some things we need to consider. But first let us have a look at fable transpiled code."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["The following code will use the ",(0,s.jsx)(n.a,{href:"https://fable.io/repl/",children:"Fable REPL"})," to transpile code for easy showcasing!"]})}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'let helloWorld = printfn "Hello World"\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { printf, toConsole } from "fable-library-js/String.js";\n\nexport const helloWorld = toConsole(printf("Hello World"));\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'from fable_library_js.string import (to_console, printf)\n\nhello_world: None = to_console(printf("Hello World"))\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"We can already notice some things:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Fable tries to transpile into native syntax, so for example snake_case in Python and camelCase in JavaScript"}),"\n",(0,s.jsxs)(n.li,{children:["Fable has some wrappers for functions which might have native equivalents. In F# ",(0,s.jsx)(n.code,{children:"printfn"})," is used to print to the console, in JavaScript ",(0,s.jsx)(n.code,{children:"console.log"})," and in Python ",(0,s.jsx)(n.code,{children:"print"}),". But Fable uses their own printf function to ensure 100% correct f# transpilation."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Next we will have a look at a class with some member functions."}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"type MyClass =\n static member add (x: int) (y: int) = x + y\n"})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n}\n\nexport function MyClass_$reflection() {\n return class_type("Test.MyClass", undefined, MyClass);\n}\n\nexport function MyClass_add(x, y) {\n return x + y;\n}\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'from fable_library_js.reflection import (TypeInfo, class_type)\n\ndef _expr0() -> TypeInfo:\n return class_type("Test.MyClass", None, MyClass)\n\n\nclass MyClass:\n ...\n\nMyClass_reflection = _expr0\n\ndef MyClass_add(x: int, y: int) -> int:\n return x + y\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Oh no, this does not look good. Fable does a thing called ",(0,s.jsx)(n.a,{href:"https://fable.io/docs/javascript/features.html#name-mangling",children:(0,s.jsx)(n.em,{children:"name mangling"})}),".\nHave a look at the offical docs for a deeper view on this topic.\nFor now its enought to know, this is done to allow overloading functions in F#."]}),"\n",(0,s.jsx)(n.admonition,{title:"Edit- Tree-Shaking",type:"info",children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.a,{href:"https://github.com/ncave",children:"ncave"})," pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members."]})}),"\n",(0,s.jsxs)(n.h3,{id:"using-attachmembers",children:["Using ",(0,s.jsx)(n.code,{children:"[]"})]}),"\n",(0,s.jsx)(n.p,{children:"But we can tell Fable that we know what we are doing and ignore name mangling."}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"open Fable.Core\n\n[]\ntype MyClass =\n static member add (x: int) (y: int) = x + y\n"})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static add(x, y) {\n return x + y;\n }\n}\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:"from fable_library_js.reflection import (TypeInfo, class_type)\n\nclass MyClass:\n @staticmethod\n def add(x: int, y: int) -> int:\n return x + y\n"})})})]}),"\n",(0,s.jsxs)(n.p,{children:["That looks better and allows us to do the following in all 3 languages: ",(0,s.jsx)(n.code,{children:"MyClass.add"}),".\nThis is the basic design i chose to use for most user facing api. All F#/Fable code not easily usable from other languages is hidden behind a facade like this."]}),"\n",(0,s.jsx)(n.h3,{id:"c-compatibility",children:"C# Compatibility"}),"\n",(0,s.jsxs)(n.p,{children:["Strangely enough allowing C# users the same ease of use as Python and JavaScript users is the hardest.\nThis is because C# has some issues with F# ",(0,s.jsx)(n.em,{children:"optional parameters"})," and F# ",(0,s.jsx)(n.em,{children:"tuples"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"In F# we can define a function like this:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'[]\ntype flowchart =\n static member raw (txt: string) = FlowchartElement txt\n static member id (txt: string) = FlowchartElement txt\n static member node (id: string, ?name: string) : FlowchartElement = ...\n\nflowchart.node("My id")\nflowchart.node("My id", "My name")\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Using the F# function in C# will result in an error, when you try to do ",(0,s.jsx)(n.code,{children:'flowchart.node("My id")'}),", as ",(0,s.jsx)(n.code,{children:"?name"})," is a ",(0,s.jsx)(n.code,{children:"Microsoft.FSharp.Core.FSharpOption"})," without any default information."]}),"\n",(0,s.jsx)(n.p,{children:"By creating a C# access layer we can avoid this issue for C# users:"}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["The C# extensions for optional parameters and tuples are taken from ",(0,s.jsx)(n.a,{href:"https://github.com/plotly/Plotly.NET",children:"Plotly.NET"})," with the help from my dear colleague ",(0,s.jsx)(n.a,{href:"https://github.com/kMutagene",children:"Kevin Schneider"}),"."]})}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"flow",label:"Flowchart.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public static class flowchart\n{\n public static FlowchartElement raw(string txt) => Siren.flowchart.raw(txt);\n\n public static FlowchartElement id(string txt) => Siren.flowchart.raw(txt);\n\n public static FlowchartElement node(string id, Optional name = default) =>\n Siren.flowchart.node(id, name.ToOption());\n //...\n}\n"})})}),(0,s.jsx)(r.A,{value:"fsharp",label:"OptionExtension.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'/// \n/// Helper type for handling the special way the Plotly.NET core API uses generics.\n/// In short, the problem arises because many optional parameters of Plotly.NET\'s core API are generics \n/// with a type constraint for `IConvertible`. This means that these parameters can be both value and reference types \n/// (e.g. `double` and `System.DateTime` both implement IConvertible).\n/// If we now have a optional parameter of type `T? where T: IConvertible` the compiler will not allow this \n/// without further type constrainst to eithe reference or value type.\n/// This is a problem because we want to 1. allow both, and 2. have a reliable way of determining if the value was not set \n/// because the F# API expects to be passed `Option.None` in that case.\n/// There exist other workarounds like checking if the value is default or null, but that changes valid default values actually set to null as well.\n/// \n/// \n/// The value to mark as optional\n/// Wether or not the wrapped value is valid. This is used downstream to determine wether to wrap this value into `Option.Some` (if true) or `Option.None` (if false)\npublic readonly record struct Optional(T Value, bool IsSome)\n{\n /// \n /// \n /// \n /// \n public static implicit operator Optional(T Value) => new(Value, true);\n\n}\n/// \n/// Extension methods for the `Optional` class\n/// \npublic static class OptionalExtensions\n{\n /// \n /// Converts the `Optional` value to `Some(value)` if the value is valid, or `None` if it is not.\n /// \n /// \n /// The `Optional` value to convert to a F# Option\n /// opt converted to `Option`\n static internal Microsoft.FSharp.Core.FSharpOption ToOption(this Optional opt) => opt.IsSome ? new(opt.Value) : Microsoft.FSharp.Core.FSharpOption.None;\n}\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"A similar issue arises with F# tuples:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"flow",label:"Flowchart.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"[]\ntype flowchart =\n static member stylesNode (nodeId: string, styles:#seq) = Flowchart.formatNodeStyles [nodeId] (List.ofSeq styles) |> FlowchartElement\n"})})}),(0,s.jsx)(r.A,{value:"extension",label:"TupleExtension.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"/// \n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\n/// \ninternal static class TupleExtensions\n{\n /// \n /// Converts a tuple.\n /// \n internal static Tuple ToTuple(this ValueTuple t) => Tuple.Create(t.Item1, t.Item2);\n}\n"})})}),(0,s.jsx)(r.A,{value:"fsharp",label:"Flowchart.cs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"/// \n/// Convenience to convert from C# struct tuple literals to the value tuple ones.\n/// \npublic static class flowchart\n{\n public static FlowchartElement stylesNode(string nodeId, (string, string)[] styles) =>\n Siren.flowchart.stylesNode(nodeId, styles.Select(t => t.ToTuple()));\n}\n"})})})]}),"\n",(0,s.jsx)(n.h4,{id:"code-generator-helper",children:"Code generator helper"}),"\n",(0,s.jsx)(n.p,{children:"These incompatibilities are not only annoying but providing a consistently native C# experience, requires a wrapping for all apis.\nTo make this easier, i created a code generator that takes a F# file and generates the C# wrapper for it. .. Or at least 95% of it. The rest is done by hand."}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Code Generator"}),(0,s.jsx)("div",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open System.Reflection\n\n[]\nlet FSharpOptionDefault = "Optional"\n\nlet transformParameterTypeName (paramTypeName: string)=\n match paramTypeName with\n | "String" -> "string"\n | "Int32" -> "int"\n | "Double" -> "double"\n | "FSharpOption`1" -> FSharpOptionDefault // this is not always true but a good approximation\n | "Tuple`2" -> "(string,string)" // this is not always true but a good approximation\n | "Boolean" -> "bool"\n | _ -> paramTypeName\n\ntype ParameterInfo = {\n Type: string\n Name: string\n} with\n member this.FSharpParam =\n match this with\n | {Type = FSharpOptionDefault} -> this.Name + ".ToOption()"\n | _ -> this.Name\n member this.CSharpParam =\n match this with\n | {Type = FSharpOptionDefault} -> sprintf "%s %s = default" this.Type this.Name\n | _ -> sprintf "%s %s" this.Type this.Name\n\n static member create(typeName: string, name: string) =\n {Type = transformParameterTypeName typeName; Name = name}\n\nlet generateCSharpCode<\'A>() =\n\n let t = typeof<\'A>\n let members = t.GetMethods(BindingFlags.Static ||| BindingFlags.Public)\n\n let mutable csharpCode = sprintf "public static class %s\\n{\\n" t.Name\n for m in members do\n let methodName = \n let name0 = m.Name\n if name0.StartsWith("get_") then\n name0.Substring(4)\n else\n name0\n let returnType = m.ReturnType.Name\n let params0 = \n m.GetParameters() \n |> Array.map (fun p -> ParameterInfo.create(p.ParameterType.Name, p.Name))\n let csharpParameters =\n if params0.Length = 0 then\n ""\n else \n params0\n |> Array.map _.CSharpParam\n |> String.concat(", ")\n |> fun s -> "(" + s + ")"\n \n let fsharpParameters = \n if params0.Length = 0 then\n ""\n else \n params0 \n |> Array.map _.FSharpParam\n |> String.concat(", ")\n |> fun s -> "(" + s + ")"\n \n let methodSignature = $"public static {transformParameterTypeName returnType} {methodName}{csharpParameters}"\n let methodBody = \n if methodName.StartsWith("get_") then\n let withoutGet = methodName.Substring(4)\n $"return Siren.{t.Name}.{withoutGet};"\n else\n $" => Siren.{t.Name}.{methodName}{fsharpParameters};"\n csharpCode <- csharpCode + $" {methodSignature}\\n {methodBody}\\n"\n\n csharpCode <- csharpCode + "}\\n"\n csharpCode\n\nlet test() = \n generateCSharpCode() // Here you can pass any type you want to generate C# code for\n |> printfn "%A"\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"This is something i did not want to spend a lot of time on, so when i quickly wrote this and noticed it was able to create most of the C# code correctly.\nI think improving this code to create everything perfectly would be awesome, but would have taken me longer thant fixing the few mistakes it makes by hand."}),"\n",(0,s.jsx)(n.h4,{id:"maintainability",children:"Maintainability"}),"\n",(0,s.jsx)(n.p,{children:"This is a big issue. Whenever i update my f# api i must also update the c# wrapper.\nChanges are mostly catched by the compiler but missings functions are not."}),"\n",(0,s.jsx)(n.p,{children:"That is why i added unit tests to check the count and name of a c# and f# class and compare it for equality:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public static class Utils\n{\n public static int GetMemberCount(Type type)\n {\n var members = type.GetMembers();\n return members.Length;\n }\n\n public static List GetMemberNameDifferences(Type type1, Type type2)\n {\n List differences = new List();\n //transform string to lower\n var type1Members = type1.GetMembers().Select(m => m.Name.ToLower());\n var type2Members = type2.GetMembers().Select(m => m.Name.ToLower());\n differences.AddRange(type1Members.Except(type2Members));\n differences.AddRange(type2Members.Except(type1Members));\n\n return differences;\n }\n\n public static void CompareClasses(Type csharpType, Type fsharpType)\n {\n int csharpMemberCount = GetMemberCount(csharpType);\n int fsharpMemberCount = GetMemberCount(fsharpType);\n List differences = GetMemberNameDifferences(fsharpType, csharpType);\n\n Assert.Empty(differences);\n Assert.Equal(fsharpMemberCount, csharpMemberCount);\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"This at least helps me to catch missing functions in the c# wrapper, even if i still have to write them by hand."}),"\n",(0,s.jsx)(n.h3,{id:"pythonjavascript-import-and-index-files",children:"Python/JavaScript import and Index files"}),"\n",(0,s.jsxs)(n.p,{children:["F# and C# use namespaces to organize code. I can have multiple files with the same namespace and access all functions simply by writing ",(0,s.jsx)(n.code,{children:"open Siren"}),"/",(0,s.jsx)(n.code,{children:"using Siren.Sea;"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"In Python and JavaScript this is not possible. Here imports happen on a file basis. So i needed to have a single file that imports all other files and exports them."}),"\n",(0,s.jsx)(n.p,{children:"Luckily this file can be created automatically!"}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Index File"}),(0,s.jsx)("div",{children:(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"util",label:"Util.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.Util\n\nopen System\nopen System.IO\nopen System.Text.RegularExpressions\n\ntype FileInformation = {\n FilePath : string\n Lines : string []\n} with\n static member create(filePath: string, lines: string []) = {\n FilePath = filePath\n Lines = lines\n }\n\nlet getAllFiles(path: string, extension: string) = \n let options = EnumerationOptions()\n options.RecurseSubdirectories <- true\n IO.Directory.EnumerateFiles(path,extension,options)\n |> Seq.filter (fun s -> s.Contains("fable_modules") |> not)\n |> Array.ofSeq\n\nlet findClasses (rootPath: string) (cois: string []) (regexPattern: string -> string) (filePaths: seq) = \n let files = [|\n for fp in filePaths do\n yield FileInformation.create(fp, System.IO.File.ReadAllLines (fp))\n |]\n let importStatements = ResizeArray()\n let findClass (className: string) = \n /// maybe set this as default if you do not want any whitelist\n let classNameDefault = @"[a-zA-Z_0-9]"\n let pattern = Regex.Escape(className) |> regexPattern\n let regex = Regex(pattern)\n let mutable found = false\n let mutable result = None\n let enum = files.GetEnumerator()\n while not found && enum.MoveNext() do\n let fileInfo = enum.Current :?> FileInformation\n for line in fileInfo.Lines do\n let m = regex.Match(line)\n match m.Success with\n | true -> \n found <- true\n result <- Some <| (className, IO.Path.GetRelativePath(rootPath,fileInfo.FilePath))\n | false ->\n ()\n match result with\n | None ->\n failwithf "Unable to find %s" className\n | Some r ->\n importStatements.Add r\n for coi in cois do findClass coi\n importStatements\n |> Array.ofSeq\n\nlet writeIndexFile (path: string) (fileName: string) (content: string) =\n let filePath = Path.Combine(path, fileName)\n File.WriteAllText(filePath, content)\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"Indexjs.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.JS\n\nlet private getAllJsFiles (path: string) fileExtension = \n Util.getAllFiles(path,$"*.{fileExtension}")\n\nlet private pattern (className: string) = sprintf @"^export class (?%s)+[\\s{].*({)?" className\n\nlet private findJsClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \n Util.findClasses rootPath whiteList pattern filePaths\n\nopen System.Text\n\nlet private createImportStatements (info: (string*string) []) =\n let sb = StringBuilder()\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\n for filePath, imports in importCollection do\n let p = filePath.Replace("\\\\","/").Replace("ts","js")\n sb.Append "export { " |> ignore\n sb.AppendJoin(", ", imports) |> ignore\n sb.Append " } from " |> ignore\n sb.Append (sprintf "\\"./%s\\"" p) |> ignore\n sb.Append ";" |> ignore\n sb.AppendLine() |> ignore\n sb.ToString()\n\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string [], fileExtension: string) =\n getAllJsFiles rootPath fileExtension\n |> findJsClasses rootPath whiteList\n |> createImportStatements\n |> Util.writeIndexFile rootPath fileName\n\nlet generate(rootPath: string) (ts: bool) = \n let extension = if ts then "ts" else "js"\n generateIndexfile(rootPath, $"index.{extension}", WhiteList.WhiteList, extension)\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Indexpy.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'let private getAllJsFiles(path: string) = \n Util.getAllFiles(path,"*.py")\n\nlet private pattern (className: string) = sprintf @"^class (?%s)+(\\(|:).*$" className\n\nlet private findPyClasses (rootPath: string) (whiteList: string []) (filePaths: string []) = \n Util.findClasses rootPath whiteList pattern filePaths\n\nopen System.Text\n\nlet private createImportStatements (info: (string*string) []) =\n let sb = StringBuilder()\n let importCollection = info |> Array.groupBy snd |> Array.map (fun (p,a) -> p, a |> Array.map fst )\n for filePath, imports in importCollection do\n let p = filePath |> Path.GetFileNameWithoutExtension\n sb.Append (sprintf "from .%s import " p) |> ignore\n sb.AppendJoin(", ", imports) |> ignore\n sb.AppendLine() |> ignore\n sb.ToString()\n\nlet private generateIndexfile (rootPath: string, fileName: string, whiteList: string []) =\n getAllJsFiles(rootPath)\n |> findPyClasses rootPath whiteList\n |> createImportStatements\n |> Util.writeIndexFile rootPath fileName\n\nlet generate(rootPath: string) (name: string) = \n // code to make camelcase to snakecase\n /// This is because we currently snake_case everything that does not start with a capital letter\n let camelCaseToSnakeCase (str: string) = \n if Char.IsUpper str.[0] then\n str \n else\n str \n |> Seq.fold (fun (acc: string) c -> \n if Char.IsUpper c then \n acc + "_" + string (Char.ToLower c) \n else \n acc + string c\n ) ""\n let snake_case_white_list = WhiteList.WhiteList |> Array.map camelCaseToSnakeCase\n generateIndexfile(rootPath, name, snake_case_white_list)\n'})})}),(0,s.jsx)(r.A,{value:"whitelist",label:"WhiteList.fs",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'module Index.WhiteList\n\nlet WhiteList = [|\n "SirenElement"\n // ThemeVaruiables\n "themeVariable"\n "quadrantTheme"; \n "gitTheme"; \n "timelineTheme"; \n "xyChartTheme"; \n "pieTheme";\n // Config\n "graphConfig";\n "flowchartConfig"; \n "sequenceConfig"; \n "ganttConfig"; \n "journeyConfig"; \n "timelineConfig"; \n "classConfig"; \n "stateConfig"; \n "erConfig"; \n "quadrantChartConfig"; \n "pieConfig"; \n "sankeyConfig"; \n "xyChartConfig"; \n "mindmapConfig"; \n "gitGraphConfig"; \n "requirementConfig"; \n // Graphs and Helpers\n "formatting"; \n "direction"; \n "flowchart"; \n "notePosition"; \n "sequence"; \n "classMemberVisibility"; \n "classMemberClassifier"; \n "classDirection"; \n "classCardinality"; \n "classRltsType"; \n "classDiagram"; \n "stateDiagram"; \n "erKey"; \n "erCardinality"; \n "erDiagram"; \n "journey"; \n "ganttTime"; \n "ganttTags"; \n "ganttUnit"; \n "gantt"; \n "pieChart"; \n "quadrant"; \n "rqRisk"; \n "rqMethod"; \n "requirement"; \n "gitType"; \n "git"; \n "mindmap"; \n "timeline"; \n "sankey"; \n "xyChart"; \n "block"; \n "theme"; \n "siren"; \n|]\n'})})})]})})]}),"\n",(0,s.jsx)(n.p,{children:"The resulting files look like this:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"js",label:"index.js",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'export { SirenElement } from "./SirenTypes.js";\nexport { themeVariable, quadrantTheme, gitTheme, timelineTheme, xyChartTheme, pieTheme } from "./ThemeVariables.js";\nexport { graphConfig, flowchartConfig, sequenceConfig, ganttConfig, journeyConfig, timelineConfig, classConfig, stateConfig, erConfig, quadrantChartConfig, pieConfig, sankeyConfig, xyChartConfig, mindmapConfig, gitGraphConfig, requirementConfig } from "./Config.js";\nexport { formatting, direction, flowchart, notePosition, sequence, classMemberVisibility, classMemberClassifier, classDirection, classCardinality, classRltsType, classDiagram, stateDiagram, erKey, erCardinality, erDiagram, journey, ganttTime, ganttTags, ganttUnit, gantt, pieChart, quadrant, rqRisk, rqMethod, requirement, gitType, git, mindmap, timeline, sankey, xyChart, block, theme, siren } from "./Siren.js";\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"index.py/__init__.py",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-py",children:"from .siren_types import SirenElement\nfrom .theme_variables import theme_variable, quadrant_theme, git_theme, timeline_theme, xy_chart_theme, pie_theme\nfrom .config import graph_config, flowchart_config, sequence_config, gantt_config, journey_config, timeline_config, class_config, state_config, er_config, quadrant_chart_config, pie_config, sankey_config, xy_chart_config, mindmap_config, git_graph_config, requirement_config\nfrom .siren import formatting, direction, flowchart, note_position, sequence, class_member_visibility, class_member_classifier, class_direction, class_cardinality, class_rlts_type, class_diagram, state_diagram, er_key, er_cardinality, er_diagram, journey, gantt_time, gantt_tags, gantt_unit, gantt, pie_chart, quadrant, rq_risk, rq_method, requirement, git_type, git, mindmap, timeline, sankey, xy_chart, block, theme, siren\n"})})})]}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["Python packages will default import whatever is in the ",(0,s.jsx)(n.code,{children:"__init__.py"})," file. So we must simply name the index file for python as such (at least for publishing)."]})}),"\n",(0,s.jsx)(n.h2,{id:"publish",children:"Publish"}),"\n",(0,s.jsx)(n.p,{children:"As soon as we get to the publishing step everything is back to standard for the respective languages."}),"\n",(0,s.jsx)(n.h3,{id:"net",children:".NET"}),"\n",(0,s.jsxs)(n.p,{children:["The easiest. We already have the required project files, no need to transpile. So we can simply use ",(0,s.jsx)(n.code,{children:"dotnet pack"})," to create .nupkg files.\nThen ",(0,s.jsx)(n.code,{children:"dotnet nuget push"})," to push to ",(0,s.jsx)(n.a,{href:"https://www.nuget.org",children:"nuget.org"}),"."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h3,{id:"pypi",children:"Pypi"}),"\n",(0,s.jsxs)(n.p,{children:["Also quite easy. Transpile f# code to python code, copy ",(0,s.jsx)(n.code,{children:"pyproject.toml"})," and ",(0,s.jsx)(n.code,{children:"README.md"})," into the ",(0,s.jsx)(n.code,{children:"dist"})," folder and create ",(0,s.jsx)(n.code,{children:"index.py"})," file.\nRun ",(0,s.jsx)(n.code,{children:"python -m poetry build"})," to create a publishing files. Then publish files with ",(0,s.jsx)(n.code,{children:"python -m poetry publish"})]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h3,{id:"javascript-types",children:"JavaScript (+Types)"}),"\n",(0,s.jsxs)(n.p,{children:["For this i followed the Example of ",(0,s.jsx)(n.a,{href:"https://fable.io/blog/2023/2023-04-20-Better_Typed_than_Sorry.html",children:"Better Typed than Sorry"})," by ",(0,s.jsx)(n.a,{href:"https://github.com/alfonsogarciacaro",children:"Alfonso Garc\xeda-Caro"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Transpile F# to TypeScript, then use ",(0,s.jsx)(n.code,{children:"tsc"})," to transpile TypeScript to JavaScript and type information files (",(0,s.jsx)(n.code,{children:"*.d.ts"}),"). Add the ",(0,s.jsx)(n.code,{children:"index.js"})," file to the ",(0,s.jsx)(n.code,{children:"dist folder"}),"\nThen publish to npm with ",(0,s.jsx)(n.code,{children:"npm publish"}),"."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"These are not the exact CLI args used. Details can be found in the build project in the GitHub repository."})}),"\n",(0,s.jsx)(n.h2,{id:"deep-dive",children:"Deep Dive"}),"\n",(0,s.jsx)(n.p,{children:"From here on are some additional issues i encountered during development."}),"\n",(0,s.jsx)(n.h3,{id:"overloads",children:"Overloads"}),"\n",(0,s.jsxs)(n.p,{children:["The concept of allowing different inputs for the same function exists in f#, as well as python and javascript.\nBy using the ",(0,s.jsx)(n.code,{children:"[]"})," attribute we are not longer allowed to use standard f# overloads.\nBecause JavaScript does not have the same kind of type interference it is unable to recognice which function should be invoked:"]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\n\n[]\ntype MyClass =\n static member add (x: int, y: int) = x * y // this should be invoked\n static member add (x: string, y: string) = x + y\n\nlet result = MyClass.add(10, 20)\n\nprintfn "%A" result\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\nimport { printf, toConsole } from "fable-library-js/String.js";\n\nexport class MyClass {\n constructor() {\n }\n static add(x, y) {\n return x * y;\n }\n static add(x, y) { // This shadows the function above and is invoked\n return x + y;\n }\n}\n\nexport const result = MyClass.add(10, 20); // This will return 30\n\ntoConsole(printf("%A"))(result);\n\n'})})})]}),"\n",(0,s.jsx)(n.h4,{id:"erased-unions",children:"Erased Unions"}),"\n",(0,s.jsxs)(n.p,{children:["It is possible to imitate js overload behaviour by using ",(0,s.jsx)(n.a,{href:"https://fable.io/docs/javascript/features.html#erased-unions",children:"erased unions"}),".\nHere we use a Fable provides discriminate union called ",(0,s.jsx)(n.code,{children:"U2"}),"(",(0,s.jsx)(n.code,{children:"U3"}),", ",(0,s.jsx)(n.code,{children:"U4"}),"...). After transpilation it is replaced by a js type check."]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"fsharp",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\nopen Fable.Core.JsInterop\n\n[]\ntype MyClass =\n static member test (arg: U2) = \n match arg with\n | U2.Case1 s -> printfn "This is a string: %s" s\n | U2.Case2 i -> printfn "This is a integer: %i" i\n\nlet result = MyClass.test(U2.Case2 10) // or MyClass.test(!^10)\n\nprintfn "%A" result\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { printf, toConsole } from "fable-library-js/String.js";\nimport { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static test(arg) {\n if (typeof arg === "number") {\n const i = arg;\n toConsole(printf("This is a integer: %i"))(i);\n }\n else {\n const s = arg;\n toConsole(printf("This is a string: %s"))(s);\n }\n }\n}\n\nexport const result = MyClass.test(10);\n\ntoConsole(printf("%A"))(); // This is a integer: 10\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"As you can see, this looks really nice in JavaScript, but is cumbersome to use in F#."}),"\n",(0,s.jsx)(n.p,{children:"It would be possible to do do both and use f# overloads and shadow them with the erased union. But this would add more additional maintainance work."}),"\n",(0,s.jsxs)(n.admonition,{title:"Compiler Statements",type:"info",children:[(0,s.jsxs)(n.p,{children:["We can use ",(0,s.jsx)(n.code,{children:"#if FABLE_COMPILER ... #else ... #endif"})," syntax to include code only in certain compiler states."]}),(0,s.jsx)(n.p,{children:"In the following example the erased union is only used (and accessible!) when the code is transpiled by Fable to JavaScript."})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'[]\ntype MyClass =\n// highlight-next-line\n#if FABLE_COMPILER_JAVASCRIPT\n static member test (arg: U2) = \n match arg with\n | U2.Case1 s -> printfn "This is a string: %s" s\n | U2.Case2 i -> printfn "This is a integer: %i" i\n// highlight-next-line\n#else\n static member test(arg: int) =\n printfn "This is a integer: %i" arg\n static member test(arg: string) =\n printfn "This is a string: %s" arg\n// highlight-next-line\n#endif\n\nlet result = MyClass.test(10)\n'})}),"\n",(0,s.jsx)(n.p,{children:"This would allow us to use the erased union only in JavaScript and use the F# overloads in F#. But i have not investigated how this would work for python \ud83d\ude05."}),"\n",(0,s.jsx)(n.p,{children:"Due to the additional workload i decided to avoid using overloads in the api. Instead i tried finding the core functions and functions, which allow additional inputs with a different name:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"type sequence =\n static member note(id: string, text: string, ?notePosition: NotePosition) = //..\n static member noteSpanning(id1: string, id2, text: string, ?notePosition: NotePosition) = //..\n"})}),"\n",(0,s.jsx)(n.h3,{id:"javascript-optional-parameters",children:"JavaScript optional parameters"}),"\n",(0,s.jsx)(n.p,{children:"Using functions with multiple optional parameters is easily done in F#, C# and Python, but can get quite annoying in JavaScript:"}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'// tripple null ...\nrequirement.requirement("my id", null, null, null, rqMethod.test)\n'})})}),(0,s.jsx)(r.A,{value:"f",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'// easy!\nrequirement.requirement("My Id", rqMethod = rqMethod.analysis)\n'})})}),(0,s.jsx)(r.A,{value:"py",label:"Python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'# easy!\nrequirement.requirement("My Id", rq_method = rq_method.analysis)\n'})})}),(0,s.jsx)(r.A,{value:"c",label:"C#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// easy!\nRequirement.requirement("My Id", rqMethod: rqMethod.analysis)\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["The JavaScript native approach would be using an object with only the values you want to set.\nThere is even a way to tell Fable to transpile parameters as object using the ",(0,s.jsx)(n.code,{children:"[ParamObject]"})," attribute."]}),"\n",(0,s.jsxs)(i.A,{children:[(0,s.jsx)(r.A,{value:"f",label:"F#",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:'open Fable.Core\nopen Fable.Core.JsInterop\n\n[]\ntype MyClass =\n [] // Start creating obj from params at index 1\n static member test (name: int, ?id: string, ?text: string, ?rqRisk: string, ?rqMethod: string) =\n 0\n\n\nMyClass.test(10, rqRisk = "Hello")\nMyClass.test(10)\n'})})}),(0,s.jsx)(r.A,{value:"js",label:"JavaScript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:'import { class_type } from "fable-library-js/Reflection.js";\n\nexport class MyClass {\n constructor() {\n }\n static test(name, { id, text, rqRisk, rqMethod }) {\n return 0;\n }\n}\n\nMyClass.test(10, {\n rqRisk: "Hello",\n});\n\nMyClass.test(10, {}); // Oh oh. Why an empty object?\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["As you can see adding ",(0,s.jsx)(n.em,{children:"no"})," optional parameters requires an empty object, as Fable checks if the value at object key xyz is null and not if the object is null.\nWithout the empty object the function would throw an error, whenever any of the optional parameters is referenced in the function. (Bad example as i just return 0)."]}),"\n",(0,s.jsx)(n.h3,{id:"member-names",children:"Member Names"}),"\n",(0,s.jsx)(n.p,{children:"Different languages have different expectations for member names. Aside from styling best practises, there are some things that are not possible in all languages."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsx)(n.p,{children:"F# typically uses PascalCase for class names and camelCase for member names.\nFor easier usage i ignore this rule and used camelCase for everything."})}),"\n",(0,s.jsx)(n.h4,{id:"f-reserved-keywords",children:"F# reserved keywords"}),"\n",(0,s.jsx)(n.p,{children:"The first issue i encountered where reserved keywords in F#."}),"\n",(0,s.jsxs)(n.p,{children:["For example ",(0,s.jsx)(n.code,{children:"classDiagram.``class``"}),". ",(0,s.jsx)(n.em,{children:'"class"'})," is a reserved keyword which is not allowed in F#.\nThe standard solution is wrapping the name in backticks. But at least for me on VisualStudio Community this resulted in issues with my auto complete.\nThis resulted in me handling this issue inconsistently. The issues i encountered were mostly in (optional) parameters, which is why i changed their names to PascalCase:"]}),"\n",(0,s.jsx)(n.p,{children:"For members i mostly stayed true to the backtick syntax."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-fsharp",children:"[]\ntype classMemberClassifier =\n // abstract is a reserved keyword\n static member Abstract = ClassMemberClassifier.Abstract\n // static is a reserved keyword\n static member Static = ClassMemberClassifier.Static\n static member custom str = ClassMemberClassifier.Custom str\n\n[]\ntype classDiagram =\n static member ``class`` (id: string, members: #seq) = \n"})}),"\n",(0,s.jsxs)(n.p,{children:["The C# wrapper used the C# best practise syntax ",(0,s.jsx)(n.code,{children:"@class"}),", which worked fine for me."]}),"\n",(0,s.jsx)(n.h4,{id:"c---member-name--enclosing-type",children:"C# - Member name = enclosing type"}),"\n",(0,s.jsxs)(n.p,{children:["I encountered this issue first for ",(0,s.jsx)(n.code,{children:"block.block"}),". In C# member names are not allowed to be the same as the enclosing type."]}),"\n",(0,s.jsxs)(n.p,{children:["As i am not a very experienced C# developer, i am still rather undecided on how to handle this issue.\nSo far I have been using ",(0,s.jsx)(n.code,{children:"Block.block"}),", as I am thinking about using PascalCase for all classes in C#. And if only to mute the warnings in VS Community."]}),"\n",(0,s.jsx)(n.p,{children:"If you have a strong opinion about this topic, please let me know! I am interested in hearing your thoughts."}),"\n",(0,s.jsx)(n.h4,{id:"transpiled-names",children:"Transpiled names"}),"\n",(0,s.jsxs)(n.p,{children:["And back to ",(0,s.jsx)(n.code,{children:"classDiagram.``class`` "}),". While JavaScript does not seem to care about this topic to much, Python does."]}),"\n",(0,s.jsxs)(n.p,{children:["JavaScript gives us the best result, the F# backtick syntax is transpiled to a simple camelCase name ",(0,s.jsx)(n.code,{children:"classDiagram.class"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Python on the other hand has ",(0,s.jsx)(n.code,{children:"class"})," also as reserved keyword. Fable transpiles it to ",(0,s.jsx)(n.code,{children:"classDiagram.class_"}),".\nWhich raises the question if i should simply apply this syntax to all cases with naming problems."]}),"\n",(0,s.jsx)(n.h3,{id:"docsnative-test-maintainance",children:"Docs/Native Test Maintainance"}),"\n",(0,s.jsxs)(n.p,{children:["The core library + C# wrapper were done rather quickly. I can also recycle my F# unit tests to check if transpilation works as expected, using ",(0,s.jsx)(n.a,{href:"https://github.com/Freymaurer/Fable.Pyxpecto",children:"Fable.Pyxpecto"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"But testing correct native accessibillity and writing docs (showcasing the test cases) was the most time consuming part and something i am not happy with."}),"\n",(0,s.jsx)(n.p,{children:"Here are some ideas on how to improve this:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Theoretically, i could use the transpiled tests for docs. But i still have to remove the Fable specific helper functions and replace them with native ones."}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.em,{children:(0,s.jsx)(n.a,{href:"https://github.com/kMutagene",children:"Kevin S."})})," had an idea, repurposing jupyter notebooks for docs and testing. To at least unify the testing and docs."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"If you have any ideas on how to improve this, please let me know!"}),"\n"]})}function m(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(p,{...e})}):p(e)}},9365:(e,n,t)=>{t.d(n,{A:()=>r});t(6540);var s=t(4164);const a={tabItem:"tabItem_Ymn6"};var i=t(4848);function r(e){let{children:n,hidden:t,className:r}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,s.A)(a.tabItem,r),hidden:t,children:n})}},1470:(e,n,t)=>{t.d(n,{A:()=>w});var s=t(6540),a=t(4164),i=t(3104),r=t(6347),l=t(205),o=t(7485),c=t(1682),h=t(9466);function d(e){return s.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function p(e){const{values:n,children:t}=e;return(0,s.useMemo)((()=>{const e=n??function(e){return d(e).map((e=>{let{props:{value:n,label:t,attributes:s,default:a}}=e;return{value:n,label:t,attributes:s,default:a}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function m(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function u(e){let{queryString:n=!1,groupId:t}=e;const a=(0,r.W6)(),i=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,o.aZ)(i),(0,s.useCallback)((e=>{if(!i)return;const n=new URLSearchParams(a.location.search);n.set(i,e),a.replace({...a.location,search:n.toString()})}),[i,a])]}function f(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,i=p(e),[r,o]=(0,s.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!m({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const s=t.find((e=>e.default))??t[0];if(!s)throw new Error("Unexpected error: 0 tabValues");return s.value}({defaultValue:n,tabValues:i}))),[c,d]=u({queryString:t,groupId:a}),[f,g]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,i]=(0,h.Dv)(t);return[a,(0,s.useCallback)((e=>{t&&i.set(e)}),[t,i])]}({groupId:a}),y=(()=>{const e=c??f;return m({value:e,tabValues:i})?e:null})();(0,l.A)((()=>{y&&o(y)}),[y]);return{selectedValue:r,selectValue:(0,s.useCallback)((e=>{if(!m({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);o(e),d(e),g(e)}),[d,g,i]),tabValues:i}}var g=t(2303);const y={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var b=t(4848);function x(e){let{className:n,block:t,selectedValue:s,selectValue:r,tabValues:l}=e;const o=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),h=e=>{const n=e.currentTarget,t=o.indexOf(n),a=l[t].value;a!==s&&(c(n),r(a))},d=e=>{let n=null;switch(e.key){case"Enter":h(e);break;case"ArrowRight":{const t=o.indexOf(e.currentTarget)+1;n=o[t]??o[0];break}case"ArrowLeft":{const t=o.indexOf(e.currentTarget)-1;n=o[t]??o[o.length-1];break}}n?.focus()};return(0,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":t},n),children:l.map((e=>{let{value:n,label:t,attributes:i}=e;return(0,b.jsx)("li",{role:"tab",tabIndex:s===n?0:-1,"aria-selected":s===n,ref:e=>o.push(e),onKeyDown:d,onClick:h,...i,className:(0,a.A)("tabs__item",y.tabItem,i?.className,{"tabs__item--active":s===n}),children:t??n},n)}))})}function j(e){let{lazy:n,children:t,selectedValue:a}=e;const i=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=i.find((e=>e.props.value===a));return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:i.map(((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==a})))})}function v(e){const n=f(e);return(0,b.jsxs)("div",{className:(0,a.A)("tabs-container",y.tabList),children:[(0,b.jsx)(x,{...e,...n}),(0,b.jsx)(j,{...e,...n})]})}function w(e){const n=(0,g.A)();return(0,b.jsx)(v,{...e,children:d(e.children)},String(n))}},8453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>l});var s=t(6540);const a={},i=s.createContext(a);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/fa4d91bf.4e7afb4d.js b/assets/js/fa4d91bf.4e7afb4d.js new file mode 100644 index 0000000..7069016 --- /dev/null +++ b/assets/js/fa4d91bf.4e7afb4d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2802],{1284:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>u,contentTitle:()=>o,default:()=>p,frontMatter:()=>i,metadata:()=>c,toc:()=>d});var r=t(4848),s=t(8453),a=t(1470),l=t(9365);const i={sidebar_label:"Installation",sidebar_position:1},o="Installation",c={id:"installation",title:"Installation",description:"Siren is available for use in JavaScript (with type support), Python, F# and C#. Depending on your preferred language you can use any best practises of installation.",source:"@site/docs/installation.mdx",sourceDirName:".",slug:"/installation",permalink:"/Siren/docs/installation",draft:!1,unlisted:!1,editUrl:"https://github.com/Freymaurer/Siren/tree/main/docs/docs/installation.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_label:"Installation",sidebar_position:1},sidebar:"docsSidebar",next:{title:"Config",permalink:"/Siren/docs/config"}},u={},d=[];function h(e){const n={code:"code",h1:"h1",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(n.p,{children:"Siren is available for use in JavaScript (with type support), Python, F# and C#. Depending on your preferred language you can use any best practises of installation."}),"\n",(0,r.jsx)(n.p,{children:"You can find the latest releases below:"}),"\n",(0,r.jsxs)("table",{children:[(0,r.jsx)("thead",{children:(0,r.jsxs)("tr",{children:[(0,r.jsx)("th",{children:"Latest Release"}),(0,r.jsx)("th",{children:"Downloads"})]})}),(0,r.jsxs)("tbody",{children:[(0,r.jsxs)("tr",{children:[(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://pypi.org/project/siren-dsl/",children:(0,r.jsx)("img",{src:"https://img.shields.io/pypi/v/siren-dsl?logo=pypi",alt:"latest release"})})}),(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://pepy.tech/project/siren-dsl",children:(0,r.jsx)("img",{alt:"Pepy Total Downlods",src:"https://img.shields.io/pepy/dt/siren-dsl?label=siren-dsl&color=blue"})})})]}),(0,r.jsxs)("tr",{children:[(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.npmjs.com/package/siren-dsl",children:(0,r.jsx)("img",{src:"https://img.shields.io/npm/v/siren-dsl?logo=npm",alt:"latest release"})})}),(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.npmjs.com/package/siren-dsl",children:(0,r.jsx)("img",{src:"https://img.shields.io/npm/dt/siren-dsl?label=siren-dsl",alt:"downloads"})})})]}),(0,r.jsxs)("tr",{children:[(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.nuget.org/packages/Siren/",children:(0,r.jsx)("img",{src:"https://img.shields.io/nuget/v/Siren?logo=nuget",alt:"latest release"})})}),(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.nuget.org/packages/Siren/",children:(0,r.jsx)("img",{src:"https://img.shields.io/nuget/dt/Siren?label=Siren",alt:"downloads"})})})]}),(0,r.jsxs)("tr",{children:[(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.nuget.org/packages/Siren.Sea/",children:(0,r.jsx)("img",{src:"https://img.shields.io/nuget/v/Siren.Sea?logo=nuget",alt:"latest release"})})}),(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.nuget.org/packages/Siren.Sea/",children:(0,r.jsx)("img",{src:"https://img.shields.io/nuget/dt/Siren.Sea?label=Siren.Sea",alt:"downloads"})})})]})]})]}),"\n",(0,r.jsx)(n.h1,{id:"reference",children:"Reference"}),"\n",(0,r.jsxs)(a.A,{groupId:"preferred-lang",queryString:!0,children:[(0,r.jsx)(l.A,{value:"fsharp",label:"F# (.fsx)",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-fsharp",children:'#r "nuget: Siren"\n\nopen Siren\n\nlet graph =\n siren.git [\n git.commit()\n ]\n |> siren.write\n\nprintfn "%s" graph\n'})})}),(0,r.jsx)(l.A,{value:"csharp",label:"C# (.ipynb)",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'// Using Polyglot Notebooks extensions\n#r "nuget:Siren.Sea"\n\nusing Siren.Sea;\n\nstring graph = \n siren.git([\n git.commit(),\n ]).write();\n\nConsole.Write(graph);\n'})})}),(0,r.jsx)(l.A,{value:"py",label:"Python",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:"from siren_dsl import siren, git\n\nstr = (\n siren.git([\n git.commit()\n ]).write()\n)\n\nprint(str)\n"})})}),(0,r.jsx)(l.A,{value:"js",label:"JavaScript",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"import {siren, git} from 'siren-dsl';\n\nconst str = siren.git([\n git.commit()\n]).write();\n\nconsole.log(str);\n"})})})]})]})}function p(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},9365:(e,n,t)=>{t.d(n,{A:()=>l});t(6540);var r=t(4164);const s={tabItem:"tabItem_Ymn6"};var a=t(4848);function l(e){let{children:n,hidden:t,className:l}=e;return(0,a.jsx)("div",{role:"tabpanel",className:(0,r.A)(s.tabItem,l),hidden:t,children:n})}},1470:(e,n,t)=>{t.d(n,{A:()=>y});var r=t(6540),s=t(4164),a=t(3104),l=t(6347),i=t(205),o=t(7485),c=t(1682),u=t(9466);function d(e){return r.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:n,children:t}=e;return(0,r.useMemo)((()=>{const e=n??function(e){return d(e).map((e=>{let{props:{value:n,label:t,attributes:r,default:s}}=e;return{value:n,label:t,attributes:r,default:s}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function p(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function g(e){let{queryString:n=!1,groupId:t}=e;const s=(0,l.W6)(),a=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,o.aZ)(a),(0,r.useCallback)((e=>{if(!a)return;const n=new URLSearchParams(s.location.search);n.set(a,e),s.replace({...s.location,search:n.toString()})}),[a,s])]}function m(e){const{defaultValue:n,queryString:t=!1,groupId:s}=e,a=h(e),[l,o]=(0,r.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const r=t.find((e=>e.default))??t[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:a}))),[c,d]=g({queryString:t,groupId:s}),[m,f]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,a]=(0,u.Dv)(t);return[s,(0,r.useCallback)((e=>{t&&a.set(e)}),[t,a])]}({groupId:s}),b=(()=>{const e=c??m;return p({value:e,tabValues:a})?e:null})();(0,i.A)((()=>{b&&o(b)}),[b]);return{selectedValue:l,selectValue:(0,r.useCallback)((e=>{if(!p({value:e,tabValues:a}))throw new Error(`Can't select invalid tab value=${e}`);o(e),d(e),f(e)}),[d,f,a]),tabValues:a}}var f=t(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var x=t(4848);function j(e){let{className:n,block:t,selectedValue:r,selectValue:l,tabValues:i}=e;const o=[],{blockElementScrollPositionUntilNextRender:c}=(0,a.a_)(),u=e=>{const n=e.currentTarget,t=o.indexOf(n),s=i[t].value;s!==r&&(c(n),l(s))},d=e=>{let n=null;switch(e.key){case"Enter":u(e);break;case"ArrowRight":{const t=o.indexOf(e.currentTarget)+1;n=o[t]??o[0];break}case"ArrowLeft":{const t=o.indexOf(e.currentTarget)-1;n=o[t]??o[o.length-1];break}}n?.focus()};return(0,x.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.A)("tabs",{"tabs--block":t},n),children:i.map((e=>{let{value:n,label:t,attributes:a}=e;return(0,x.jsx)("li",{role:"tab",tabIndex:r===n?0:-1,"aria-selected":r===n,ref:e=>o.push(e),onKeyDown:d,onClick:u,...a,className:(0,s.A)("tabs__item",b.tabItem,a?.className,{"tabs__item--active":r===n}),children:t??n},n)}))})}function v(e){let{lazy:n,children:t,selectedValue:s}=e;const a=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=a.find((e=>e.props.value===s));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return(0,x.jsx)("div",{className:"margin-top--md",children:a.map(((e,n)=>(0,r.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function w(e){const n=m(e);return(0,x.jsxs)("div",{className:(0,s.A)("tabs-container",b.tabList),children:[(0,x.jsx)(j,{...e,...n}),(0,x.jsx)(v,{...e,...n})]})}function y(e){const n=(0,f.A)();return(0,x.jsx)(w,{...e,children:d(e.children)},String(n))}},8453:(e,n,t)=>{t.d(n,{R:()=>l,x:()=>i});var r=t(6540);const s={},a=r.createContext(s);function l(e){const n=r.useContext(a);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:l(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/fa4d91bf.ab1de002.js b/assets/js/fa4d91bf.ab1de002.js deleted file mode 100644 index af35fa0..0000000 --- a/assets/js/fa4d91bf.ab1de002.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2802],{1284:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>u,contentTitle:()=>o,default:()=>p,frontMatter:()=>l,metadata:()=>c,toc:()=>d});var r=t(4848),s=t(8453),a=t(1470),i=t(9365);const l={sidebar_label:"Installation",sidebar_position:1},o="Installation",c={id:"installation",title:"Installation",description:"Siren is available for use in JavaScript (with type support), Python, F# and C#. Depending on your preferred language you can use any best practises of installation.",source:"@site/docs/installation.mdx",sourceDirName:".",slug:"/installation",permalink:"/Siren/docs/installation",draft:!1,unlisted:!1,editUrl:"https://github.com/Freymaurer/Siren/tree/main/docs/docs/installation.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_label:"Installation",sidebar_position:1},sidebar:"docsSidebar",next:{title:"Config",permalink:"/Siren/docs/config"}},u={},d=[];function h(e){const n={code:"code",h1:"h1",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(n.p,{children:"Siren is available for use in JavaScript (with type support), Python, F# and C#. Depending on your preferred language you can use any best practises of installation."}),"\n",(0,r.jsx)(n.p,{children:"You can find the latest releases below:"}),"\n",(0,r.jsxs)("table",{children:[(0,r.jsx)("thead",{children:(0,r.jsxs)("tr",{children:[(0,r.jsx)("th",{children:"Latest Release"}),(0,r.jsx)("th",{children:"Downloads"})]})}),(0,r.jsxs)("tbody",{children:[(0,r.jsxs)("tr",{children:[(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://pypi.org/project/siren-dsl/",children:(0,r.jsx)("img",{src:"https://img.shields.io/pypi/v/siren-dsl?logo=pypi",alt:"latest release"})})}),(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://pepy.tech/project/siren-dsl",children:(0,r.jsx)("img",{src:"https://pepy.tech/badge/siren-dsl",alt:"downloads"})})})]}),(0,r.jsxs)("tr",{children:[(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.npmjs.com/package/siren-dsl",children:(0,r.jsx)("img",{src:"https://img.shields.io/npm/v/siren-dsl?logo=npm",alt:"latest release"})})}),(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.npmjs.com/package/siren-dsl",children:(0,r.jsx)("img",{src:"https://img.shields.io/npm/dt/siren-dsl.svg",alt:"downloads"})})})]}),(0,r.jsxs)("tr",{children:[(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.nuget.org/packages/Siren/",children:(0,r.jsx)("img",{src:"https://img.shields.io/nuget/v/Siren?logo=nuget",alt:"latest release"})})}),(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.nuget.org/packages/Siren/",children:(0,r.jsx)("img",{src:"https://img.shields.io/nuget/dt/Siren.svg",alt:"downloads"})})})]}),(0,r.jsxs)("tr",{children:[(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.nuget.org/packages/Siren.Sea/",children:(0,r.jsx)("img",{src:"https://img.shields.io/nuget/v/Siren.Sea?logo=nuget",alt:"latest release"})})}),(0,r.jsx)("td",{children:(0,r.jsx)("a",{href:"https://www.nuget.org/packages/Siren.Sea/",children:(0,r.jsx)("img",{src:"https://img.shields.io/nuget/dt/Siren.Sea.svg",alt:"downloads"})})})]})]})]}),"\n",(0,r.jsx)(n.h1,{id:"reference",children:"Reference"}),"\n",(0,r.jsxs)(a.A,{groupId:"preferred-lang",queryString:!0,children:[(0,r.jsx)(i.A,{value:"fsharp",label:"F# (.fsx)",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-fsharp",children:'#r "nuget: Siren"\n\nopen Siren\n\nlet graph =\n siren.git [\n git.commit()\n ]\n |> siren.write\n\nprintfn "%s" graph\n'})})}),(0,r.jsx)(i.A,{value:"csharp",label:"C# (.ipynb)",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'// Using Polyglot Notebooks extensions\n#r "nuget:Siren.Sea"\n\nusing Siren.Sea;\n\nstring graph = \n siren.git([\n git.commit(),\n ]).write();\n\nConsole.Write(graph);\n'})})}),(0,r.jsx)(i.A,{value:"py",label:"Python",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:"from siren_dsl import siren, git\n\nstr = (\n siren.git([\n git.commit()\n ]).write()\n)\n\nprint(str)\n"})})}),(0,r.jsx)(i.A,{value:"js",label:"JavaScript",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-js",children:"import {siren, git} from 'siren-dsl';\n\nconst str = siren.git([\n git.commit()\n]).write();\n\nconsole.log(str);\n"})})})]})]})}function p(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},9365:(e,n,t)=>{t.d(n,{A:()=>i});t(6540);var r=t(4164);const s={tabItem:"tabItem_Ymn6"};var a=t(4848);function i(e){let{children:n,hidden:t,className:i}=e;return(0,a.jsx)("div",{role:"tabpanel",className:(0,r.A)(s.tabItem,i),hidden:t,children:n})}},1470:(e,n,t)=>{t.d(n,{A:()=>y});var r=t(6540),s=t(4164),a=t(3104),i=t(6347),l=t(205),o=t(7485),c=t(1682),u=t(9466);function d(e){return r.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:n,children:t}=e;return(0,r.useMemo)((()=>{const e=n??function(e){return d(e).map((e=>{let{props:{value:n,label:t,attributes:r,default:s}}=e;return{value:n,label:t,attributes:r,default:s}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function p(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function g(e){let{queryString:n=!1,groupId:t}=e;const s=(0,i.W6)(),a=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,o.aZ)(a),(0,r.useCallback)((e=>{if(!a)return;const n=new URLSearchParams(s.location.search);n.set(a,e),s.replace({...s.location,search:n.toString()})}),[a,s])]}function m(e){const{defaultValue:n,queryString:t=!1,groupId:s}=e,a=h(e),[i,o]=(0,r.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const r=t.find((e=>e.default))??t[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:a}))),[c,d]=g({queryString:t,groupId:s}),[m,f]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,a]=(0,u.Dv)(t);return[s,(0,r.useCallback)((e=>{t&&a.set(e)}),[t,a])]}({groupId:s}),b=(()=>{const e=c??m;return p({value:e,tabValues:a})?e:null})();(0,l.A)((()=>{b&&o(b)}),[b]);return{selectedValue:i,selectValue:(0,r.useCallback)((e=>{if(!p({value:e,tabValues:a}))throw new Error(`Can't select invalid tab value=${e}`);o(e),d(e),f(e)}),[d,f,a]),tabValues:a}}var f=t(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var x=t(4848);function j(e){let{className:n,block:t,selectedValue:r,selectValue:i,tabValues:l}=e;const o=[],{blockElementScrollPositionUntilNextRender:c}=(0,a.a_)(),u=e=>{const n=e.currentTarget,t=o.indexOf(n),s=l[t].value;s!==r&&(c(n),i(s))},d=e=>{let n=null;switch(e.key){case"Enter":u(e);break;case"ArrowRight":{const t=o.indexOf(e.currentTarget)+1;n=o[t]??o[0];break}case"ArrowLeft":{const t=o.indexOf(e.currentTarget)-1;n=o[t]??o[o.length-1];break}}n?.focus()};return(0,x.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.A)("tabs",{"tabs--block":t},n),children:l.map((e=>{let{value:n,label:t,attributes:a}=e;return(0,x.jsx)("li",{role:"tab",tabIndex:r===n?0:-1,"aria-selected":r===n,ref:e=>o.push(e),onKeyDown:d,onClick:u,...a,className:(0,s.A)("tabs__item",b.tabItem,a?.className,{"tabs__item--active":r===n}),children:t??n},n)}))})}function v(e){let{lazy:n,children:t,selectedValue:s}=e;const a=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=a.find((e=>e.props.value===s));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return(0,x.jsx)("div",{className:"margin-top--md",children:a.map(((e,n)=>(0,r.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function w(e){const n=m(e);return(0,x.jsxs)("div",{className:(0,s.A)("tabs-container",b.tabList),children:[(0,x.jsx)(j,{...e,...n}),(0,x.jsx)(v,{...e,...n})]})}function y(e){const n=(0,f.A)();return(0,x.jsx)(w,{...e,children:d(e.children)},String(n))}},8453:(e,n,t)=>{t.d(n,{R:()=>i,x:()=>l});var r=t(6540);const s={},a=r.createContext(s);function i(e){const n=r.useContext(a);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.60460aff.js b/assets/js/runtime~main.5550df67.js similarity index 69% rename from assets/js/runtime~main.60460aff.js rename to assets/js/runtime~main.5550df67.js index cc20aa9..8ad25cc 100644 --- a/assets/js/runtime~main.60460aff.js +++ b/assets/js/runtime~main.5550df67.js @@ -1 +1 @@ -(()=>{"use strict";var e,f,a,d,c,b={},t={};function r(e){var f=t[e];if(void 0!==f)return f.exports;var a=t[e]={exports:{}};return b[e].call(a.exports,a,a.exports,r),a.exports}r.m=b,e=[],r.O=(f,a,d,c)=>{if(!a){var b=1/0;for(i=0;i=c)&&Object.keys(r.O).every((e=>r.O[e](a[o])))?a.splice(o--,1):(t=!1,c0&&e[i-1][2]>c;i--)e[i]=e[i-1];e[i]=[a,d,c]},r.n=e=>{var f=e&&e.__esModule?()=>e.default:()=>e;return r.d(f,{a:f}),f},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,d){if(1&d&&(e=this(e)),8&d)return e;if("object"==typeof e&&e){if(4&d&&e.__esModule)return e;if(16&d&&"function"==typeof e.then)return e}var c=Object.create(null);r.r(c);var b={};f=f||[null,a({}),a([]),a(a)];for(var t=2&d&&e;"object"==typeof t&&!~f.indexOf(t);t=a(t))Object.getOwnPropertyNames(t).forEach((f=>b[f]=()=>e[f]));return b.default=()=>e,r.d(c,b),c},r.d=(e,f)=>{for(var a in f)r.o(f,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:f[a]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((f,a)=>(r.f[a](e,f),f)),[])),r.u=e=>"assets/js/"+({120:"9adb0d94",387:"68e3f9c8",1248:"01314738",1345:"978ba77e",1458:"65487120",1536:"19bb12e7",1903:"acecf23e",1949:"ddff72c0",1961:"9df2d1e7",1972:"afe017b7",2224:"f8551aab",2338:"5e06daff",2711:"9e4087bc",2802:"fa4d91bf",2849:"2dba065b",3088:"03181aef",3130:"186ef366",3249:"ccc49370",3411:"7127de71",3449:"df937a0d",3608:"455735a5",3811:"32d2d526",4134:"393be207",4229:"5fc483ef",4583:"1df93b7f",4813:"6875c492",4865:"5dc2cc4d",5044:"546169ec",5717:"7ed95570",5939:"f8a3f497",5999:"974f9b32",6061:"1f391b9e",6308:"01f0d703",6362:"64f9a9b0",6593:"7136f0cb",6996:"2cf82806",7098:"a7bd4aaa",7466:"831440af",7472:"814f3328",7475:"57502b2a",7617:"20338fc9",7643:"a6aa9e1f",8168:"616b0b73",8209:"01a85c17",8222:"ddaeecbe",8240:"984a9349",8367:"d76234ee",8401:"17896441",8498:"1f632201",8581:"935f2afb",9048:"a94703ab",9124:"2dfb1d4f",9193:"4198046f",9221:"22f88faa",9552:"ba9ff1e5",9647:"5e95c892"}[e]||e)+"."+{4:"f664605d",120:"53426a9a",387:"7fbd59a6",751:"9cc42fea",1169:"2f7c8835",1176:"e2530ce7",1248:"7d963d2e",1345:"a16c612b",1458:"ab7e23bd",1536:"cf0be019",1555:"d35b963b",1903:"62e98cba",1949:"80b662b7",1961:"e67d1955",1972:"bbef699f",2130:"6c9f8cee",2224:"2b45f0f8",2235:"5f850352",2237:"703d927a",2317:"39673a30",2338:"85b2920d",2711:"f82ed01d",2746:"3c44cea7",2802:"ab1de002",2849:"d9a04bce",3088:"6c101a43",3130:"7d0e3b49",3242:"cf261a87",3249:"6fd92043",3411:"ba55569d",3449:"89519e84",3608:"9ff5da7a",3811:"10fbf80a",3863:"c1dde917",4134:"64be851c",4229:"2338b4be",4583:"7535ef83",4813:"80128212",4865:"8bc73b71",5044:"a6fdaf6d",5642:"3c1cd188",5688:"dacba012",5717:"54a15c29",5829:"170c7d20",5939:"959755f4",5999:"b4420d2e",6061:"a26b3314",6063:"42520c96",6216:"7e0973aa",6292:"dd3d44d7",6308:"699aa6f7",6362:"7d16c98b",6506:"3d03b296",6593:"d17b0f1d",6732:"c770a299",6940:"3273203c",6946:"e7cfaf0f",6996:"90da3ef4",7098:"b74e8d88",7121:"73bc8998",7147:"b77abd27",7200:"86e9142b",7211:"e6739559",7308:"b04f4955",7440:"4d741548",7466:"66ef3cd6",7472:"cec43cbc",7475:"c7a2ac91",7617:"f82249a0",7643:"4fa468d7",8168:"64b6b0e5",8209:"7cae7edf",8222:"31607180",8240:"63231122",8327:"972fe4f3",8367:"c427c0b8",8401:"e9355a92",8498:"54864d43",8581:"b49bd50d",8609:"5669fbd0",8747:"ae2c99a7",8947:"be4a5373",9048:"52cd7b0f",9124:"5e41823f",9193:"406b3f2a",9221:"74eede57",9469:"e3da4e05",9552:"91893519",9647:"ea8da11b",9688:"f0f8ae6b"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,f)=>Object.prototype.hasOwnProperty.call(e,f),d={},c="docs:",r.l=(e,f,a,b)=>{if(d[e])d[e].push(f);else{var t,o;if(void 0!==a)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var c=d[e];if(delete d[e],t.parentNode&&t.parentNode.removeChild(t),c&&c.forEach((e=>e(a))),f)return f(a)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/Siren/",r.gca=function(e){return e={17896441:"8401",65487120:"1458","9adb0d94":"120","68e3f9c8":"387","01314738":"1248","978ba77e":"1345","19bb12e7":"1536",acecf23e:"1903",ddff72c0:"1949","9df2d1e7":"1961",afe017b7:"1972",f8551aab:"2224","5e06daff":"2338","9e4087bc":"2711",fa4d91bf:"2802","2dba065b":"2849","03181aef":"3088","186ef366":"3130",ccc49370:"3249","7127de71":"3411",df937a0d:"3449","455735a5":"3608","32d2d526":"3811","393be207":"4134","5fc483ef":"4229","1df93b7f":"4583","6875c492":"4813","5dc2cc4d":"4865","546169ec":"5044","7ed95570":"5717",f8a3f497:"5939","974f9b32":"5999","1f391b9e":"6061","01f0d703":"6308","64f9a9b0":"6362","7136f0cb":"6593","2cf82806":"6996",a7bd4aaa:"7098","831440af":"7466","814f3328":"7472","57502b2a":"7475","20338fc9":"7617",a6aa9e1f:"7643","616b0b73":"8168","01a85c17":"8209",ddaeecbe:"8222","984a9349":"8240",d76234ee:"8367","1f632201":"8498","935f2afb":"8581",a94703ab:"9048","2dfb1d4f":"9124","4198046f":"9193","22f88faa":"9221",ba9ff1e5:"9552","5e95c892":"9647"}[e]||e,r.p+r.u(e)},(()=>{var e={5354:0,1869:0};r.f.j=(f,a)=>{var d=r.o(e,f)?e[f]:void 0;if(0!==d)if(d)a.push(d[2]);else if(/^(1869|5354)$/.test(f))e[f]=0;else{var c=new Promise(((a,c)=>d=e[f]=[a,c]));a.push(d[2]=c);var b=r.p+r.u(f),t=new Error;r.l(b,(a=>{if(r.o(e,f)&&(0!==(d=e[f])&&(e[f]=void 0),d)){var c=a&&("load"===a.type?"missing":a.type),b=a&&a.target&&a.target.src;t.message="Loading chunk "+f+" failed.\n("+c+": "+b+")",t.name="ChunkLoadError",t.type=c,t.request=b,d[1](t)}}),"chunk-"+f,f)}},r.O.j=f=>0===e[f];var f=(f,a)=>{var d,c,b=a[0],t=a[1],o=a[2],n=0;if(b.some((f=>0!==e[f]))){for(d in t)r.o(t,d)&&(r.m[d]=t[d]);if(o)var i=o(r)}for(f&&f(a);n{"use strict";var e,f,a,d,c,t={},b={};function r(e){var f=b[e];if(void 0!==f)return f.exports;var a=b[e]={exports:{}};return t[e].call(a.exports,a,a.exports,r),a.exports}r.m=t,e=[],r.O=(f,a,d,c)=>{if(!a){var t=1/0;for(i=0;i=c)&&Object.keys(r.O).every((e=>r.O[e](a[o])))?a.splice(o--,1):(b=!1,c0&&e[i-1][2]>c;i--)e[i]=e[i-1];e[i]=[a,d,c]},r.n=e=>{var f=e&&e.__esModule?()=>e.default:()=>e;return r.d(f,{a:f}),f},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,d){if(1&d&&(e=this(e)),8&d)return e;if("object"==typeof e&&e){if(4&d&&e.__esModule)return e;if(16&d&&"function"==typeof e.then)return e}var c=Object.create(null);r.r(c);var t={};f=f||[null,a({}),a([]),a(a)];for(var b=2&d&&e;"object"==typeof b&&!~f.indexOf(b);b=a(b))Object.getOwnPropertyNames(b).forEach((f=>t[f]=()=>e[f]));return t.default=()=>e,r.d(c,t),c},r.d=(e,f)=>{for(var a in f)r.o(f,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:f[a]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((f,a)=>(r.f[a](e,f),f)),[])),r.u=e=>"assets/js/"+({120:"9adb0d94",387:"68e3f9c8",1248:"01314738",1345:"978ba77e",1458:"65487120",1536:"19bb12e7",1903:"acecf23e",1949:"ddff72c0",1961:"9df2d1e7",1972:"afe017b7",2224:"f8551aab",2338:"5e06daff",2711:"9e4087bc",2802:"fa4d91bf",2849:"2dba065b",3088:"03181aef",3130:"186ef366",3249:"ccc49370",3411:"7127de71",3449:"df937a0d",3608:"455735a5",3811:"32d2d526",4134:"393be207",4229:"5fc483ef",4583:"1df93b7f",4813:"6875c492",4865:"5dc2cc4d",5044:"546169ec",5717:"7ed95570",5939:"f8a3f497",5999:"974f9b32",6061:"1f391b9e",6308:"01f0d703",6362:"64f9a9b0",6593:"7136f0cb",6996:"2cf82806",7098:"a7bd4aaa",7466:"831440af",7472:"814f3328",7475:"57502b2a",7617:"20338fc9",7643:"a6aa9e1f",8168:"616b0b73",8209:"01a85c17",8222:"ddaeecbe",8240:"984a9349",8367:"d76234ee",8401:"17896441",8498:"1f632201",8581:"935f2afb",9048:"a94703ab",9124:"2dfb1d4f",9193:"4198046f",9221:"22f88faa",9552:"ba9ff1e5",9647:"5e95c892"}[e]||e)+"."+{4:"f664605d",120:"53426a9a",387:"7fbd59a6",751:"9cc42fea",1169:"2f7c8835",1176:"e2530ce7",1248:"7d963d2e",1345:"a16c612b",1458:"ab7e23bd",1536:"cf0be019",1555:"d35b963b",1903:"62e98cba",1949:"80b662b7",1961:"e67d1955",1972:"bbef699f",2130:"6c9f8cee",2224:"2b45f0f8",2235:"5f850352",2237:"703d927a",2317:"39673a30",2338:"6d3b9cb0",2711:"f82ed01d",2746:"3c44cea7",2802:"4e7afb4d",2849:"d9a04bce",3088:"6c101a43",3130:"7d0e3b49",3242:"cf261a87",3249:"6fd92043",3411:"ba55569d",3449:"89519e84",3608:"9ff5da7a",3811:"10fbf80a",3863:"c1dde917",4134:"64be851c",4229:"2338b4be",4583:"7535ef83",4813:"80128212",4865:"8bc73b71",5044:"a6fdaf6d",5642:"3c1cd188",5688:"dacba012",5717:"54a15c29",5829:"170c7d20",5939:"959755f4",5999:"b4420d2e",6061:"a26b3314",6063:"42520c96",6216:"7e0973aa",6292:"dd3d44d7",6308:"699aa6f7",6362:"7d16c98b",6506:"3d03b296",6593:"d17b0f1d",6732:"c770a299",6940:"3273203c",6946:"e7cfaf0f",6996:"90da3ef4",7098:"b74e8d88",7121:"73bc8998",7147:"b77abd27",7200:"86e9142b",7211:"e6739559",7308:"b04f4955",7440:"4d741548",7466:"c04aa112",7472:"cec43cbc",7475:"c7a2ac91",7617:"f82249a0",7643:"4fa468d7",8168:"64b6b0e5",8209:"7cae7edf",8222:"31607180",8240:"ac019b8e",8327:"972fe4f3",8367:"c427c0b8",8401:"e9355a92",8498:"54864d43",8581:"b49bd50d",8609:"5669fbd0",8747:"ae2c99a7",8947:"be4a5373",9048:"52cd7b0f",9124:"5e41823f",9193:"406b3f2a",9221:"74eede57",9469:"e3da4e05",9552:"91893519",9647:"ea8da11b",9688:"f0f8ae6b"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,f)=>Object.prototype.hasOwnProperty.call(e,f),d={},c="docs:",r.l=(e,f,a,t)=>{if(d[e])d[e].push(f);else{var b,o;if(void 0!==a)for(var n=document.getElementsByTagName("script"),i=0;i{b.onerror=b.onload=null,clearTimeout(s);var c=d[e];if(delete d[e],b.parentNode&&b.parentNode.removeChild(b),c&&c.forEach((e=>e(a))),f)return f(a)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:b}),12e4);b.onerror=l.bind(null,b.onerror),b.onload=l.bind(null,b.onload),o&&document.head.appendChild(b)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/Siren/",r.gca=function(e){return e={17896441:"8401",65487120:"1458","9adb0d94":"120","68e3f9c8":"387","01314738":"1248","978ba77e":"1345","19bb12e7":"1536",acecf23e:"1903",ddff72c0:"1949","9df2d1e7":"1961",afe017b7:"1972",f8551aab:"2224","5e06daff":"2338","9e4087bc":"2711",fa4d91bf:"2802","2dba065b":"2849","03181aef":"3088","186ef366":"3130",ccc49370:"3249","7127de71":"3411",df937a0d:"3449","455735a5":"3608","32d2d526":"3811","393be207":"4134","5fc483ef":"4229","1df93b7f":"4583","6875c492":"4813","5dc2cc4d":"4865","546169ec":"5044","7ed95570":"5717",f8a3f497:"5939","974f9b32":"5999","1f391b9e":"6061","01f0d703":"6308","64f9a9b0":"6362","7136f0cb":"6593","2cf82806":"6996",a7bd4aaa:"7098","831440af":"7466","814f3328":"7472","57502b2a":"7475","20338fc9":"7617",a6aa9e1f:"7643","616b0b73":"8168","01a85c17":"8209",ddaeecbe:"8222","984a9349":"8240",d76234ee:"8367","1f632201":"8498","935f2afb":"8581",a94703ab:"9048","2dfb1d4f":"9124","4198046f":"9193","22f88faa":"9221",ba9ff1e5:"9552","5e95c892":"9647"}[e]||e,r.p+r.u(e)},(()=>{var e={5354:0,1869:0};r.f.j=(f,a)=>{var d=r.o(e,f)?e[f]:void 0;if(0!==d)if(d)a.push(d[2]);else if(/^(1869|5354)$/.test(f))e[f]=0;else{var c=new Promise(((a,c)=>d=e[f]=[a,c]));a.push(d[2]=c);var t=r.p+r.u(f),b=new Error;r.l(t,(a=>{if(r.o(e,f)&&(0!==(d=e[f])&&(e[f]=void 0),d)){var c=a&&("load"===a.type?"missing":a.type),t=a&&a.target&&a.target.src;b.message="Loading chunk "+f+" failed.\n("+c+": "+t+")",b.name="ChunkLoadError",b.type=c,b.request=t,d[1](b)}}),"chunk-"+f,f)}},r.O.j=f=>0===e[f];var f=(f,a)=>{var d,c,t=a[0],b=a[1],o=a[2],n=0;if(t.some((f=>0!==e[f]))){for(d in b)r.o(b,d)&&(r.m[d]=b[d]);if(o)var i=o(r)}for(f&&f(a);n Library Design | Siren - + @@ -43,6 +43,7 @@

API DesignOh no, this does not look good. Fable does a thing called name mangling. Have a look at the offical docs for a deeper view on this topic. For now its enought to know, this is done to allow overloading functions in F#.

+
Edit- Tree-Shaking

ncave pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.

Using [<AttachMembers>]

But we can tell Fable that we know what we are doing and ignore name mangling.

open Fable.Core

[<AttachMembers>]
type MyClass =
static member add (x: int) (y: int) = x + y
diff --git a/blog/archive/index.html b/blog/archive/index.html index 77448bc..bfc6f09 100644 --- a/blog/archive/index.html +++ b/blog/archive/index.html @@ -5,7 +5,7 @@ Archive | Siren - + diff --git a/blog/atom.xml b/blog/atom.xml index 35a3c5b..522d1cb 100644 --- a/blog/atom.xml +++ b/blog/atom.xml @@ -47,6 +47,7 @@

Oh no, this does not look good. Fable does a thing called name mangling. Have a look at the offical docs for a deeper view on this topic. For now its enought to know, this is done to allow overloading functions in F#.

+
Edit- Tree-Shaking

ncave pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.

Using [<AttachMembers>]

But we can tell Fable that we know what we are doing and ignore name mangling.

open Fable.Core

[<AttachMembers>]
type MyClass =
static member add (x: int) (y: int) = x + y
diff --git a/blog/index.html b/blog/index.html index b92a099..5547175 100644 --- a/blog/index.html +++ b/blog/index.html @@ -5,7 +5,7 @@ Blog | Siren - + @@ -43,6 +43,7 @@

API DesignOh no, this does not look good. Fable does a thing called name mangling. Have a look at the offical docs for a deeper view on this topic. For now its enought to know, this is done to allow overloading functions in F#.

+
Edit- Tree-Shaking

ncave pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.

Using [<AttachMembers>]

But we can tell Fable that we know what we are doing and ignore name mangling.

open Fable.Core

[<AttachMembers>]
type MyClass =
static member add (x: int) (y: int) = x + y
diff --git a/blog/rss.xml b/blog/rss.xml index f82975f..e3af228 100644 --- a/blog/rss.xml +++ b/blog/rss.xml @@ -48,6 +48,7 @@

Oh no, this does not look good. Fable does a thing called name mangling. Have a look at the offical docs for a deeper view on this topic. For now its enought to know, this is done to allow overloading functions in F#.

+
Edit- Tree-Shaking

ncave pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.

Using [<AttachMembers>]

But we can tell Fable that we know what we are doing and ignore name mangling.

open Fable.Core

[<AttachMembers>]
type MyClass =
static member add (x: int) (y: int) = x + y
diff --git a/blog/tags/api/index.html b/blog/tags/api/index.html index c9d8655..8f3457f 100644 --- a/blog/tags/api/index.html +++ b/blog/tags/api/index.html @@ -5,7 +5,7 @@ One post tagged with "api" | Siren - + @@ -43,6 +43,7 @@

API DesignOh no, this does not look good. Fable does a thing called name mangling. Have a look at the offical docs for a deeper view on this topic. For now its enought to know, this is done to allow overloading functions in F#.

+
Edit- Tree-Shaking

ncave pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.

Using [<AttachMembers>]

But we can tell Fable that we know what we are doing and ignore name mangling.

open Fable.Core

[<AttachMembers>]
type MyClass =
static member add (x: int) (y: int) = x + y
diff --git a/blog/tags/design/index.html b/blog/tags/design/index.html index 1904407..0a6f8a5 100644 --- a/blog/tags/design/index.html +++ b/blog/tags/design/index.html @@ -5,7 +5,7 @@ One post tagged with "design" | Siren - + @@ -43,6 +43,7 @@

API DesignOh no, this does not look good. Fable does a thing called name mangling. Have a look at the offical docs for a deeper view on this topic. For now its enought to know, this is done to allow overloading functions in F#.

+
Edit- Tree-Shaking

ncave pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.

Using [<AttachMembers>]

But we can tell Fable that we know what we are doing and ignore name mangling.

open Fable.Core

[<AttachMembers>]
type MyClass =
static member add (x: int) (y: int) = x + y
diff --git a/blog/tags/fable/index.html b/blog/tags/fable/index.html index 7d597bc..2600fb4 100644 --- a/blog/tags/fable/index.html +++ b/blog/tags/fable/index.html @@ -5,7 +5,7 @@ One post tagged with "fable" | Siren - + @@ -43,6 +43,7 @@

API DesignOh no, this does not look good. Fable does a thing called name mangling. Have a look at the offical docs for a deeper view on this topic. For now its enought to know, this is done to allow overloading functions in F#.

+
Edit- Tree-Shaking

ncave pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.

Using [<AttachMembers>]

But we can tell Fable that we know what we are doing and ignore name mangling.

open Fable.Core

[<AttachMembers>]
type MyClass =
static member add (x: int) (y: int) = x + y
diff --git a/blog/tags/helper/index.html b/blog/tags/helper/index.html index 42fae60..aa58f33 100644 --- a/blog/tags/helper/index.html +++ b/blog/tags/helper/index.html @@ -5,7 +5,7 @@ One post tagged with "helper" | Siren - + @@ -43,6 +43,7 @@

API DesignOh no, this does not look good. Fable does a thing called name mangling. Have a look at the offical docs for a deeper view on this topic. For now its enought to know, this is done to allow overloading functions in F#.

+
Edit- Tree-Shaking

ncave pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.

Using [<AttachMembers>]

But we can tell Fable that we know what we are doing and ignore name mangling.

open Fable.Core

[<AttachMembers>]
type MyClass =
static member add (x: int) (y: int) = x + y
diff --git a/blog/tags/index.html b/blog/tags/index.html index 99883f2..ba99e29 100644 --- a/blog/tags/index.html +++ b/blog/tags/index.html @@ -5,7 +5,7 @@ Tags | Siren - + diff --git a/blog/tags/library/index.html b/blog/tags/library/index.html index a9ed020..40c09e9 100644 --- a/blog/tags/library/index.html +++ b/blog/tags/library/index.html @@ -5,7 +5,7 @@ One post tagged with "library" | Siren - + @@ -43,6 +43,7 @@

API DesignOh no, this does not look good. Fable does a thing called name mangling. Have a look at the offical docs for a deeper view on this topic. For now its enought to know, this is done to allow overloading functions in F#.

+
Edit- Tree-Shaking

ncave pointed also out to me that tree-shaking in JavaScript was facilitated in this way. As it was easier for bundlers to detect import of single functions, rather then unused class members.

Using [<AttachMembers>]

But we can tell Fable that we know what we are doing and ignore name mangling.

open Fable.Core

[<AttachMembers>]
type MyClass =
static member add (x: int) (y: int) = x + y
diff --git a/docs/Graphs/Block/index.html b/docs/Graphs/Block/index.html index d5b544f..044ca40 100644 --- a/docs/Graphs/Block/index.html +++ b/docs/Graphs/Block/index.html @@ -5,7 +5,7 @@ Block | Siren - + diff --git a/docs/Graphs/ClassDiagram/index.html b/docs/Graphs/ClassDiagram/index.html index 7c101d2..198bcbd 100644 --- a/docs/Graphs/ClassDiagram/index.html +++ b/docs/Graphs/ClassDiagram/index.html @@ -5,7 +5,7 @@ ClassDiagram | Siren - + diff --git a/docs/Graphs/ErDiagram/index.html b/docs/Graphs/ErDiagram/index.html index e40d62c..d9749d9 100644 --- a/docs/Graphs/ErDiagram/index.html +++ b/docs/Graphs/ErDiagram/index.html @@ -5,7 +5,7 @@ ErDiagram | Siren - + diff --git a/docs/Graphs/FlowChart/index.html b/docs/Graphs/FlowChart/index.html index f08d285..bbb7ad4 100644 --- a/docs/Graphs/FlowChart/index.html +++ b/docs/Graphs/FlowChart/index.html @@ -5,7 +5,7 @@ FlowChart | Siren - + diff --git a/docs/Graphs/Gantt/index.html b/docs/Graphs/Gantt/index.html index ea9d75e..82d23a7 100644 --- a/docs/Graphs/Gantt/index.html +++ b/docs/Graphs/Gantt/index.html @@ -5,7 +5,7 @@ Gantt | Siren - + diff --git a/docs/Graphs/Git/index.html b/docs/Graphs/Git/index.html index c4eb98b..62c48bc 100644 --- a/docs/Graphs/Git/index.html +++ b/docs/Graphs/Git/index.html @@ -5,7 +5,7 @@ Git | Siren - + diff --git a/docs/Graphs/Journey/index.html b/docs/Graphs/Journey/index.html index 34f05ac..9687739 100644 --- a/docs/Graphs/Journey/index.html +++ b/docs/Graphs/Journey/index.html @@ -5,7 +5,7 @@ Journey | Siren - + diff --git a/docs/Graphs/Mindmap/index.html b/docs/Graphs/Mindmap/index.html index c5e8608..597fc47 100644 --- a/docs/Graphs/Mindmap/index.html +++ b/docs/Graphs/Mindmap/index.html @@ -5,7 +5,7 @@ Mindmap | Siren - + diff --git a/docs/Graphs/PieChart/index.html b/docs/Graphs/PieChart/index.html index c22447f..64393d1 100644 --- a/docs/Graphs/PieChart/index.html +++ b/docs/Graphs/PieChart/index.html @@ -5,7 +5,7 @@ PieChart | Siren - + diff --git a/docs/Graphs/Quadrant/index.html b/docs/Graphs/Quadrant/index.html index 32ec421..4b2ab21 100644 --- a/docs/Graphs/Quadrant/index.html +++ b/docs/Graphs/Quadrant/index.html @@ -5,7 +5,7 @@ Quadrant | Siren - + diff --git a/docs/Graphs/Requirement/index.html b/docs/Graphs/Requirement/index.html index 0bd3038..76d4db0 100644 --- a/docs/Graphs/Requirement/index.html +++ b/docs/Graphs/Requirement/index.html @@ -5,7 +5,7 @@ Requirement | Siren - + diff --git a/docs/Graphs/Sankey/index.html b/docs/Graphs/Sankey/index.html index 5c8f2bc..2f2c25c 100644 --- a/docs/Graphs/Sankey/index.html +++ b/docs/Graphs/Sankey/index.html @@ -5,7 +5,7 @@ Sankey | Siren - + diff --git a/docs/Graphs/Sequence/index.html b/docs/Graphs/Sequence/index.html index da48444..74aa807 100644 --- a/docs/Graphs/Sequence/index.html +++ b/docs/Graphs/Sequence/index.html @@ -5,7 +5,7 @@ Sequence | Siren - + diff --git a/docs/Graphs/StateDiagram/index.html b/docs/Graphs/StateDiagram/index.html index 2ff0f60..a8f2e20 100644 --- a/docs/Graphs/StateDiagram/index.html +++ b/docs/Graphs/StateDiagram/index.html @@ -5,7 +5,7 @@ StateDiagram | Siren - + diff --git a/docs/Graphs/Timeline/index.html b/docs/Graphs/Timeline/index.html index e7f0c57..b7d5587 100644 --- a/docs/Graphs/Timeline/index.html +++ b/docs/Graphs/Timeline/index.html @@ -5,7 +5,7 @@ Timeline | Siren - + diff --git a/docs/Graphs/XYChart/index.html b/docs/Graphs/XYChart/index.html index 9440725..ef0a539 100644 --- a/docs/Graphs/XYChart/index.html +++ b/docs/Graphs/XYChart/index.html @@ -5,7 +5,7 @@ XYChart | Siren - + diff --git a/docs/TabTemplates/index.html b/docs/TabTemplates/index.html index 2f08c23..ea7eed7 100644 --- a/docs/TabTemplates/index.html +++ b/docs/TabTemplates/index.html @@ -5,7 +5,7 @@ Code | Siren - + diff --git a/docs/config/index.html b/docs/config/index.html index 8fb451d..22dd214 100644 --- a/docs/config/index.html +++ b/docs/config/index.html @@ -5,7 +5,7 @@ Config | Siren - + diff --git a/docs/installation/index.html b/docs/installation/index.html index 68395b5..e9b0dea 100644 --- a/docs/installation/index.html +++ b/docs/installation/index.html @@ -5,14 +5,14 @@ Installation | Siren - +

Installation

Siren is available for use in JavaScript (with type support), Python, F# and C#. Depending on your preferred language you can use any best practises of installation.

You can find the latest releases below:

-
Latest ReleaseDownloads
latest releasedownloads
latest releasedownloads
latest releasedownloads
latest releasedownloads
+
Latest ReleaseDownloads
latest releasePepy Total Downlods
latest releasedownloads
latest releasedownloads
latest releasedownloads

Reference

#r "nuget: Siren"

open Siren

let graph =
siren.git [
git.commit()
]
|> siren.write

printfn "%s" graph
diff --git a/docs/theme/index.html b/docs/theme/index.html index 98bd8e5..7db33d4 100644 --- a/docs/theme/index.html +++ b/docs/theme/index.html @@ -5,7 +5,7 @@ Themes | Siren - + diff --git a/docs/themeVariables/index.html b/docs/themeVariables/index.html index a32bbf8..2689362 100644 --- a/docs/themeVariables/index.html +++ b/docs/themeVariables/index.html @@ -5,7 +5,7 @@ Theme Variables | Siren - + diff --git a/index.html b/index.html index 8196937..803f654 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ Hello from Siren | Siren - + diff --git a/markdown-page/index.html b/markdown-page/index.html index a506bc9..992ca8d 100644 --- a/markdown-page/index.html +++ b/markdown-page/index.html @@ -5,7 +5,7 @@ Markdown page example | Siren - + diff --git a/my-markdown-page/index.html b/my-markdown-page/index.html index 22185fb..d56dc36 100644 --- a/my-markdown-page/index.html +++ b/my-markdown-page/index.html @@ -5,7 +5,7 @@ My Markdown page | Siren - + diff --git a/my-react-page/index.html b/my-react-page/index.html index 1459071..6e18834 100644 --- a/my-react-page/index.html +++ b/my-react-page/index.html @@ -5,7 +5,7 @@ Siren - +