Skip to content

Instantly share code, notes, and snippets.

@cristianoc
Last active March 28, 2023 21:06
Show Gist options
  • Save cristianoc/d0aed6503dfb8f2421cca6df17752478 to your computer and use it in GitHub Desktop.
Save cristianoc/d0aed6503dfb8f2421cca6df17752478 to your computer and use it in GitHub Desktop.

Brainstorming Ideas for Untagged Variants

One-page summary:

  • Introduction of Untagged Variants: You propose adding a new type system feature called untagged variants, which would be an addition to the existing tagged variants feature. The syntax for untagged variants uses |: instead of |. For example, type someType = A |: B |: C(string, string).
  • Expressing Tagged Variants Using Untagged Variants: Untagged variants can represent the same types as tagged variants. For instance, a list type that would normally be expressed as a tagged variant can be expressed as an untagged variant like this: type list<'a> = undefined |: {tag:"Cons", _0: 'a, _1: list<'a>}.
  • Interaction of Untagged Variants with Type Variables: You've observed that untagged variants can be combined with type variables to express more complex types. As an example, the either type can be expressed as type either<'a, 'b> = Either('a) |: Or('b). This allows for more expressive types and greater flexibility in the type system, including the possibility of extensible polymorphic variants.
  • Expressing Nullable Types: Untagged unions can be used to express nullable types, such as types that can either be null or have a value of a specific type. For example, type nullableType<'a> = Null |: Value('a) represents a type that can be either null or have a value of type 'a.
  • Expressing Extensible Polymorphic Variants: Your ideas also include the possibility of using untagged variants with type variables to express extensible polymorphic variants. Consider the following example: type openType<'a> = First |: Second |: Open('a). If 'a is instantiated with type [#third], the resulting type effectively has three cases. However, if the type is extended with [> #third], which has an extra implicit type variable, the resulting untagged variant will have a type variable that represents at least three cases, but remains open to further extensions.

In summary, your ideas revolve around the introduction of untagged variants as an addition to the existing type system. This would allow for greater flexibility and expressiveness in the type system, enabling more complex types and the possibility of extensible polymorphic variants. The examples provided demonstrate the potential applications and benefits of your proposed ideas.


Introduction

  • Exploring a new type system feature called untagged variants
  • Presenting untagged variants as an addition to the existing tagged variants feature

Idea 1: Introduction of Untagged Variants

Description

  • A new variant syntax for the type system, using the symbol "|:" instead of "|"
  • Example of untagged variant syntax: type someType = A |: B |: C(string, string)

Benefits

  • Provides an alternative way to express existing tagged variants
  • Allows for more flexibility and expressiveness in the type system

Challenges

  • Ensuring compatibility with existing tagged variant implementations
  • Communicating the benefits and use cases of untagged variants to users

Idea 2: Expressing Tagged Variants Using Untagged Variants

Description

  • Untagged variants can be used to represent the same types as tagged variants
  • Example: A list type can be expressed using untagged variants as follows: type list<'a> = undefined |: {tag:"Cons", _0: 'a, _1: list<'a>}

Benefits

  • Simplifies the type system by allowing one variant syntax to express both tagged and untagged variants
  • Enables greater flexibility in type representation and usage

Challenges

  • Ensuring a smooth transition for users

Idea 3: Interaction of Untagged Variants with Type Variables

Description

  • Untagged variants and type variables can be used together to express more complex types, allowing for new possibilities in the type system
  • Example 1: The either type can be expressed as type either<'a, 'b> = Either('a) |: Or('b). This type can represent a value of either type 'a or type 'b.
    • When working with this type, pattern matching would require considering 'a and 'b as additional inputs to a function. Functions like isEither: 'a => bool and isOr: 'a => bool would be implicitly defined.
    • A function that matches on an either value might look like this:
      x => switch x {
        | Either(_) => "it's an Either"
        | Or(_) => "it's an Or"
      }
      
      This function has two implicit parameters for the type variables, which can be made explicit:
      (isEither, isOr, x) =>
        if (isEither(x)) { "it's an Either" }
        else { "it's an Or" }
      
    • Type instantiation corresponds to passing the implicit parameters. For example, when calling this function with Either("hello"), the type variable 'a is instantiated with type string, and the call corresponds to the following desugaring: let result = foo(/*isEither */ (v => typeof(v) === "string"), _, "hello"). In this case, foo is the pattern matching function from the previous example. This instantiation passes the isEither function, does not specify the isOr function (as it is not used), and passes the "hello" string.

Benefits

  • Allows for more expressive types and greater flexibility in the type system, enabling the creation of richer and more adaptable APIs
  • Supports advanced use cases, such as extensible polymorphic variants, which can make code more reusable and modular
  • Can lead to better code maintainability by allowing developers to express more nuanced relationships between types

Challenges

  1. Ensuring proper handling of implicit function parameters when working with type variables:

    • Possible solution: Introduce a mechanism for making implicit parameters explicit, as demonstrated in the pattern matching example, which can help in understanding and managing these parameters
    • Possible solution: Provide clear documentation and guidelines on how to work with these implicit parameters, including best practices for handling them correctly
  2. Simplifying type instantiation by making isEither and isOr optional arguments:

    • Possible solution: Introduce default implementations for these arguments that are automatically provided by the type system, allowing users to override them only when necessary
    • Possible solution: Enhance the type system to detect when these arguments are not needed and automatically remove them from the function signature, reducing the need for users to manually manage them
  3. Ensuring compatibility with existing type systems and programming languages: Possible solution: Design the untagged variant feature as an optional extension or a separate module, so that it can be adopted incrementally without breaking existing codebases Possible solution: Provide clear migration paths and guidelines for developers who want to start using untagged variants in their projects, including examples and best practices

  4. Balancing expressiveness and complexity in the type system: Possible solution: Establish clear use cases and scenarios where untagged variants offer significant benefits over traditional tagged variants, so developers can make informed decisions on when to use each approach Possible solution: Offer tooling support, such as type checkers, linters, and IDE integrations, to help developers work effectively with untagged variants and type variables

Observation 1: Expressing Nullable Types

Description

  • Untagged unions can be used to express nullable types, which are types that can represent either a null value or a value of a specific type
  • Example: An untagged union representing a nullable integer can be defined as follows: type nullableInt = Null |: Int(int). In this example, a value of type nullableInt can either be Null or Int(int) where int is an integer value

Benefits

  • Provides a more expressive and flexible way to represent optional or nullable values in the type system
  • Can simplify function signatures and data structures by allowing developers to represent the absence of a value using a null variant

Use Cases

  • Representing optional function arguments or return values: Functions can use nullable types to indicate that an argument is optional or that a return value may not always be present
  • Modeling data structures with optional fields: When designing data structures, nullable types can be used to represent fields that may not always have a value

Challenges

  • Ensuring proper handling of nullable types: Developers need to be mindful of null values and handle them correctly in their code, which may require additional error handling or null-checking
  • Balancing expressiveness and complexity: While nullable types can make code more flexible, they may also introduce additional complexity, particularly when working

Observation 2: Expressing Extensible Polymorphic Variants

Description

  • Untagged variants with type variables can be used to express extensible polymorphic variants, which are types that can be extended with new cases without modifying the original definition
  • Example: Consider the following extensible polymorphic variant definition using untagged variants: type openType<'a> = First |: Second |: Open('a) In this example, if 'a is instantiated with a specific type (e.g., Third), the resulting type effectively has three cases (First, Second, and Open with the specific type Third). If the type is extended with another type variable (e.g., 'b), the resulting untagged variant will have a type variable that represents at least three cases (First, Second, and Open with 'b), but remains open to further extensions as 'b can be instantiated with other types.

Benefits

  • Allows for more modular and flexible code by enabling types to be extended without modifying their original definitions
  • Facilitates code reuse and composition by allowing developers to build on existing types without the need for extensive refactoring or code duplication

Use Cases

  • Modeling extensible data structures: Extensible polymorphic variants can be used to represent data structures that can be extended with new cases or functionality, such as event systems, error handling, or plugin architectures
  • Implementing open-ended APIs: APIs can be designed with extensible polymorphic variants to allow for future extensions or customization without breaking existing code

Challenges

  • Ensuring proper handling of extensible polymorphic variants: Developers need to account for the possibility of additional cases when working with these types, which may require more complex pattern matching or error handling
  • Balancing express

Observation 3: Expressing Fine Tuned Runtime Representations

Untagged variants can be used to express fine-tuned runtime representations, allowing for more efficient memory usage and performance in certain cases.

Idea 1: Efficient List Representation

You can define a list type with a more efficient runtime representation using untagged variants: type list<'a> = Empty(null) |: Cons(('a, list<'a>)) Here, the Cons constructor takes a pair of values (indicated by the extra parentheses), which can be represented more efficiently as an array of size 2 in JavaScript. This allows for a more compact and performant representation of the list data structure.

A conversion function between the two list representations can be defined, which appears as the identity function but actually converts the runtime representation: let convert : list1<'a> -> list2<'a>.

Idea 2: Efficient Option Types

You can also use untagged variants to define more efficient option types that box the argument only when necessary. The option type can be parameterized with an isA function, allowing you to create a more efficient representation for the Some case: type option<'a> = parameterized with isA function. The isSome function can be defined as follows:

let isSome = (v) => {
  if (isA(null)) {
    return "v is of shape {boxed:v1}" && isA(v1);
  } else {
    return isA(v);
  }
};

This function checks if the value v is of the shape {boxed: v1} when isA(null) is true, and if it matches the expected type otherwise.

To build values, you need to modify the Some function:

let Some = (v) => (isA(null) ? {boxed: v} : v);

This function will only box the value v when necessary, providing a more efficient representation in certain cases.

However, this approach can introduce runtime overhead if the representation needs to be determined at runtime. The existing compiler optimizations for option types, such as static inlining of known isA functions, can help to mitigate this overhead.

By using the isA functions to avoid unnecessary boxing, you can achieve a more efficient runtime representation for option types. This is an improvement over the partial optimization problem, where the compiler doesn't know the definition of t, and so it boxes the value.

In summary, Observation 3 demonstrates the potential of untagged variants to allow for fine-tuned runtime representations, leading to improved performance and memory usage in specific cases. These examples illustrate the flexibility and expressiveness of untagged variants in defining custom data structures and optimizing their underlying representations.

Next Steps

  • Discuss and evaluate ideas with colleagues
  • Determine priority and feasibility of each idea
  • Develop action plan and timeline for implementation

Conclusion

  • Recap of the brainstorming session and key takeaways
  • Express gratitude for the opportunity to share ideas and collaborate

Appendix A: Untagged Variants and Type Variables Summary

Throughout our discussion, we explored the concept of untagged variants and their relationship with type variables. Untagged variants provide an alternative representation for types, allowing for more flexibility and expressiveness in certain situations. They are defined using the |: symbol, as opposed to the traditional | symbol used for tagged variants.

We began by demonstrating how untagged variants can be used to represent familiar data structures, such as lists and the either type, by combining them with type variables. We then discussed the special role type variables play in pattern matching, showing how functions like isEither and isOr are used to determine the appropriate case.

Next, we considered several observations and examples related to untagged variants:

We showed that untagged variants can be used to express nullable types, such as either null | T. We explored the idea of extensible polymorphic variants, where untagged variants and type variables can be combined to create open types that can be extended later. We discussed the potential for fine-tuned runtime representations, allowing for more efficient memory usage in certain cases, such as lists with length 0, 1, or 2. Throughout these examples, we observed that untagged variants provide a powerful way to express various types and structures. However, they also introduce some challenges, particularly when it comes to pattern matching and code generation. In some cases, the use of untagged variants may require more complex runtime type checks or more generic code to handle the unknown relations associated with type variables.

Finally, we explored how untagged variants could be used to design a type-safe JSON library, showcasing the versatility of untagged variants in practical applications. In the library design, we demonstrated how untagged variants can be used to represent JSON data structures and how pattern matching can be utilized in the implementation.

In conclusion, untagged variants and type variables offer a powerful way to express various types and structures in programming languages. They enable more flexibility and expressiveness, allowing for more efficient memory usage and extensible types. However, they also introduce some challenges related to pattern matching and code generation. As we explored these concepts and their applications, we gained a deeper understanding of the potential benefits and trade-offs involved in using untagged variants and type variables in language design.

Appendix B: Encoding Tagged Variants Using Untagged Ones

In this conversation, we discussed the possibility of representing tagged variants using untagged ones. While it is possible to do so, it introduces verbosity and requires the user to manage the tags manually. Users need to create and maintain the object types for each case and ensure the tag property is set correctly.

In contrast, native tagged variants are more concise and less error-prone, as the language handles tagging internally. Additionally, pattern matching syntax is more ergonomic when working with tagged variants, since it is designed explicitly for that purpose.

Although it is technically possible to represent tagged variants using untagged ones, it may not be the most convenient or efficient way to work with them in practice. The benefits of using native tagged variants in terms of conciseness, ease of use, and pattern matching support typically outweigh the advantages of using untagged variants for this purpose.

Appendix C: Encoding Polymorphic Variants Using Untagged Variants

In this conversation, we explored the possibility of encoding polymorphic variants using untagged variants, focusing on their extensibility and expressiveness. Untagged variants differ from polymorphic variants in that they rely on runtime type information and structure of the values, instead of using tags to distinguish cases.

Untagged variants can express extensibility in a tag-less manner by including a case with a type variable as payload. This allows the type to be extended with new cases without modifying the original type definition. Here's an example:

type untaggedFruit<'a> =
  |: Apple({ color: string })
  |: Banana({ length: int })
  |: Extra('a)

In this example, the Extra case with the type variable 'a enables the extensibility of the untaggedFruit type. By providing the appropriate payload type, we can extend the type with new cases, as shown in the example with the Orange case.

Untagged variants are strictly more expressive than polymorphic variants, as they can model tag-less, extensible types without relying on tags. However, this approach comes with trade-offs, such as potential performance implications and less concise pattern-matching syntax compared to polymorphic variants.

@cristianoc
Copy link
Author

cristianoc commented Mar 27, 2023

Can one use these untagged variants to write a type safe json library?

Using untagged variants, it is possible to write a type-safe JSON library that can help ensure the correctness of JSON data manipulation and serialization/deserialization. By leveraging the expressiveness of untagged variants, you can define a type hierarchy representing the different types of JSON values and provide type-safe functions for working with JSON data.

Here's a high-level sketch of how you could define a type-safe JSON library using untagged variants:

Define a type hierarchy for JSON values using untagged variants:

type JsonValue = JNull |: JBool(bool) |: JNumber(float) |: JString(string) |: JArray(JsonArray) |: JObject(JsonObject)

Define types for JSON arrays and objects:

type JsonArray = list<JsonValue>
type JsonObject = list<(string, JsonValue)>

Provide type-safe functions for working with JSON data:

  • Parsing JSON strings into JsonValue instances
  • Serializing JsonValue instances back into JSON strings
  • Accessing JSON object properties and array elements with appropriate type checking
  • Modifying JSON data while preserving type-safety

By using untagged variants, you can represent the different JSON value types and their relationships in a clear and concise way. This approach enables type-safe manipulation of JSON data while ensuring that invalid JSON constructs are caught at compile-time rather than runtime.

However, keep in mind that implementing a type-safe JSON library using untagged variants may introduce some complexity and performance overhead, especially when dealing with generic types and runtime type checking. It is important to carefully balance the benefits of type safety with the potential performance implications and the complexity of your implementation.

To address some of these challenges, you might consider the following:

  • Optimize runtime performance by leveraging compiler optimizations, such as inlining and partial evaluation, to reduce the overhead associated with runtime type checking and manipulation.
  • Provide efficient and easy-to-use APIs for common JSON operations, such as parsing, serialization, and data manipulation, while preserving type safety.
  • Implement custom error handling and reporting to help users of your library understand and fix type errors in their JSON data.

By focusing on these aspects, you can create a type-safe JSON library using untagged variants that provides both the benefits of type safety and the performance and usability required for working with JSON data in real-world applications.

Here's an outline of the design for a type-safe JSON library using untagged variants, along with a few sample functions to get you started:

Define the JsonValue type hierarchy:

type JsonValue = JNull |: JBool(bool) |: JNumber(float) |: JString(string) |: JArray(JsonArray) |: JObject(JsonObject)

Define types for JSON arrays and objects:

type JsonArray = list<JsonValue>
type JsonObject = list<(string, JsonValue)>

Provide type-safe functions for working with JSON data:

let parse: string -> JsonValue

The parse function takes a JSON string and returns a JsonValue representation of it. Implement this function using a JSON parsing library or write your own JSON parser that produces instances of the JsonValue type.

Serializing JsonValue instances

let serialize: JsonValue -> string

The serialize function takes a JsonValue and returns a JSON string representation. Implement this function by recursively serializing each JsonValue based on its variant type.

Accessing JSON object properties

let getObjectProperty: (JsonObject, string) -> option<JsonValue>

The getObjectProperty function takes a JsonObject and a property key, returning an option representing the value of the property if it exists.

let getObjectProperty = (obj, key) => {
  let keyValue = obj.find(([k, _]) => k == key);
  return keyValue ? Some(keyValue[1]) : None;
};

Accessing JSON array elements

let getArrayElement: (JsonArray, int) -> option<JsonValue>

The getArrayElement function takes a JsonArray and an index, returning an option representing the value at the specified index if it exists.

let getArrayElement = (arr, idx) => {
  return (idx >= 0 && idx < arr.length) ? Some(arr[idx]) : None;
};

Modifying JSON object properties

let setObjectProperty: (JsonObject, string, JsonValue) -> JsonObject

The setObjectProperty function takes a JsonObject, a property key, and a JsonValue. It returns a new JsonObject with the property set to the specified value.


Can you show an example of how pattern matching of untagged variants is used here? Is it used to implement the library or user-side?

In the type-safe JSON library using untagged variants, pattern matching can be used both for implementing the library and for user-side code that manipulates JSON data. Here, we'll focus on a user-side example that demonstrates how pattern matching with untagged variants can be employed when working with JSON data.

Let's say you have a JsonValue instance that represents a JSON object, and you want to extract a specific property value and perform different actions based on the property's type. In this case, you can use pattern matching to handle each possible JsonValue variant.

Assuming you have the following JSON object:

{
  "property": "stringValue"
}

Parsed into a JsonValue:

let json = JObject([("property", JString("stringValue"))])

You can use pattern matching to extract the "property" value and handle each possible JsonValue variant:

let handleJsonValue = (jsonValue: JsonValue) => {
  switch jsonValue {
    | JNull => print("Property is null")
    | JBool(b) => print("Property is a boolean: " ++ toString(b))
    | JNumber(n) => print("Property is a number: " ++ toString(n))
    | JString(s) => print("Property is a string: " ++ s)
    | JArray(a) => print("Property is an array")
    | JObject(o) => print("Property is an object")
  }
};

// Extract the "property" value from the JSON object
let propertyValue = getObjectProperty(json, "property");

// Handle the extracted value using pattern matching
match propertyValue {
| Some(value) => handleJsonValue(value)
| None => print("Property not found")
};

In this example, pattern matching is used to differentiate between the various JsonValue variants and perform different actions accordingly. When you extract the "property" value from the JSON object, you use the getObjectProperty function that returns an option<JsonValue>. Then, you use pattern matching to handle the case where the property is found (Some(value)) and the case where it's not found (None).

Inside the handleJsonValue function, you also use pattern matching to perform different

@cristianoc
Copy link
Author

Where is pattern matching used in the implementation of the library?

Pattern matching can be used in the implementation of the type-safe JSON library when serializing a JsonValue back into a JSON string. Here's an example of how pattern matching can be employed in the serialize function:

let serialize: JsonValue -> string

let serialize = (jsonValue: JsonValue) => {
  switch jsonValue {
    | JNull => "null"
    | JBool(b) => toString(b)
    | JNumber(n) => toString(n)
    | JString(s) => "\"" ++ s ++ "\""
    | JArray(a) => "[" ++ String.join(", ", List.map(serialize, a)) ++ "]"
    | JObject(o) => "{" ++ String.join(", ", List.map(([k, v]) => "\"" ++ k ++ "\": " ++ serialize(v), o)) ++ "}"
  }
};

In this example, pattern matching is used to handle different JsonValue variants when serializing the value back into a JSON string. The serialize function takes a JsonValue as an input and returns a string representation of the JSON data. By using pattern matching, you can easily handle each JsonValue variant separately, ensuring that the appropriate JSON string representation is generated.

@cristianoc
Copy link
Author

cristianoc commented Mar 27, 2023

The deserialize function in the type-safe JSON library using untagged variants can take advantage of the untagged nature of the variants when parsing JSON strings. Instead of relying on tags to identify the type of a value, the function can directly utilize the structure of the JSON data to construct the appropriate JsonValue variants. This can lead to a more efficient and streamlined implementation.

Here's an example of how the deserialize function could be implemented for this library:

let deserialize: string -> option<JsonValue>

The deserialize function takes a JSON string as input and returns an option representing the parsed JSON data if the input is a valid JSON string. The implementation could use a JSON parsing library or a custom parser to parse the JSON string.

As the variants are untagged, we can directly construct the corresponding JsonValue variant based on the structure of the JSON data. For example:

  1. If the parsed value is a JSON null, we can create the JNull variant directly.
  2. If the parsed value is a JSON boolean, we can create the JBool variant with the corresponding boolean value.
  3. If the parsed value is a JSON number, we can create the JNumber variant with the corresponding numeric value.
  4. If the parsed value is a JSON string, we can create the JString variant with the corresponding string value.
  5. If the parsed value is a JSON array, we can create the JArray variant with a list of JsonValue instances representing the elements of the array. We can use recursion and call the deserialize function on each element in the array.
  6. If the parsed value is a JSON object, we can create the JObject variant with a list of key-value pairs, where the key is a string and the value is a JsonValue instance. We can use recursion and call the deserialize function on the value of each key-value pair in the object.

By using untagged variants, the deserialize function can build the appropriate JsonValue variant based on the JSON data structure without the need for tags. This simplifies the parsing process and enables a more direct and efficient construction of the JsonValue instances.

Here's a high-level outline of the deserialize function implementation:

let deserialize = (jsonStr: string) => {
  let parseResult = parseJson(jsonStr); // Utilize a JSON parsing library or custom parser

  // Recursively construct JsonValue instances based on the structure of the JSON data
  let rec buildJsonValue = (parsedValue) => {
    match parsedValue {
      | ParsedNull => JNull
      | ParsedBool(b) => JBool(b)
      | ParsedNumber(n) => JNumber(n)
      | ParsedString(s) => JString(s)
      | ParsedArray(a) => JArray(List.map(buildJsonValue, a))
      | ParsedObject(o) => JObject(List.map(([k, v]) => (k, buildJsonValue(v)), o))
    }
  };

  // If parsing was successful, construct the corresponding JsonValue, otherwise return None
match parseResult {
| Some(parsedValue) => Some(buildJsonValue(parsedValue))
| None => None
}
};

In this implementation, the deserialize function uses a JSON parsing library or a custom parser to parse the input JSON string. If the parsing is successful, it constructs the corresponding JsonValue instances based on the structure of the JSON data using the buildJsonValue helper function. The helper function uses pattern matching to handle different parsed values and recursively constructs JsonValue instances.

When using untagged variants in this way, the deserialize function can take advantage of the more direct relationship between the JSON data structure and the corresponding JsonValue variants, leading to a more efficient and straightforward implementation.

@cristianoc
Copy link
Author

cristianoc commented Mar 27, 2023

In the spirit of untagged variants, I would try to avoid having to, for example, turn an object into a list. That's an expensive extra allocation.
Isn't there a way to change the type JArray so the deserialise function can return directly the result of parseJson?

Yes, you can indeed change the JArray and JObject types to avoid the extra allocation and more closely match the output of the parseJson function. One possible approach is to update the JsonValue type definition to use untagged variants for arrays and objects directly:

type JsonValue =
  |: JNull
  |: JBool(bool)
  |: JNumber(float)
  |: JString(string)
  |: JArray(array<JsonValue>)
  |: JObject(dict<string, JsonValue>)

In this definition, the JArray variant directly holds an array of JsonValue instances, and the JObject variant holds a dictionary with string keys and JsonValue values.

Now, the deserialize function can return the result of parseJson directly when handling arrays and objects, without the need for extra allocations:

let deserialize = (jsonStr: string) => {
  let parseResult = parseJson(jsonStr); // Utilize a JSON parsing library or custom parser

  // Recursively construct JsonValue instances based on the structure of the JSON data
  let rec buildJsonValue = (parsedValue) => {
    match parsedValue {
      | ParsedNull => JNull
      | ParsedBool(b) => JBool(b)
      | ParsedNumber(n) => JNumber(n)
      | ParsedString(s) => JString(s)
      | ParsedArray(a) => JArray(a.map(buildJsonValue))
      | ParsedObject(o) => JObject(o.mapValues(buildJsonValue))
    }
  };

  // If parsing was successful, construct the corresponding JsonValue, otherwise return None
  match parseResult {
    | Some(parsedValue) => Some(buildJsonValue(parsedValue))
    | None => None
  }
};

In this updated implementation, the deserialize function still uses a JSON parsing library or a custom parser to parse the input JSON string. If the parsing is successful, it constructs the corresponding JsonValue instances based on the structure of the JSON data using the buildJsonValue function directly uses the output of the parseJson function. For the JArray variant, it maps the buildJsonValue function over the parsed array, and for the JObject variant, it maps the buildJsonValue function over the values in the parsed dictionary.

By using untagged variants in this way and updating the JArray and JObject types, the deserialize function can avoid extra allocations and more directly construct the JsonValue instances based on the output of the parseJson function. This leads to a more efficient and straightforward implementation.

@cristianoc
Copy link
Author

Title: A Type-Safe JSON Library Using Untagged Variants

Abstract:

We present a type-safe JSON library utilizing untagged variants to enable efficient and straightforward manipulation of JSON data in a statically typed programming language. The library allows users to serialize and deserialize JSON data while preserving type safety, ensuring that runtime errors due to type mismatches are minimized. The core data structure, JsonValue, utilizes untagged variants to closely represent the JSON data model, including null, booleans, numbers, strings, arrays, and objects.

The untagged variants in the JsonValue type allow for a more direct relationship between the JSON data structure and the corresponding JsonValue instances, leading to an efficient and straightforward implementation of serialization and deserialization. By using untagged variants and updating the array and object types, the library avoids extra allocations when constructing JsonValue instances, reducing runtime overhead.

The library provides users with the necessary functions to parse, manipulate, and generate JSON data in a type-safe manner. Pattern matching is employed in the implementation of these functions, allowing for more readable and maintainable code. In addition, the untagged variants enable the representation of more complex and extensible data structures, providing flexibility and expressiveness for a wide range of use cases.

Overall, this type-safe JSON library leveraging untagged variants offers an efficient, extensible, and user-friendly solution for handling JSON data in a statically typed programming language. The design minimizes runtime overhead and reduces the potential for runtime errors, ensuring a robust and reliable experience for developers working with JSON data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment