C
C#2mo ago
olayk

Creating a Printer class for my own type.

I am doing this for learning and am aware that if I want to work with JSON I can use System.Text.Json This is the type to extend:
using System.Text;
namespace JSON.Types;

abstract class JSONValue {
public abstract JSONValue this[string s] { get; }
public abstract JSONValue this[int i] { get; }
}

class JSONNum(int val) : JSONValue {
public int Val { get; set; } = val;
public override JSONValue this[string s] { get => throw new IndexOutOfRangeException("Cannot index type JSONNum"); }
public override JSONValue this[int i] { get => throw new IndexOutOfRangeException("Cannot index type JSONNum"); }
}

class JSONString(string val) : JSONValue {
public string Val { get; set; } = val;
public override JSONValue this[string s] { get => throw new IndexOutOfRangeException("Cannot index type JSONString"); }
public override JSONValue this[int i] { get => throw new IndexOutOfRangeException("Cannot index type JSONString"); }
}

class JSONObject(Dictionary<string, JSONValue> val) : JSONValue {
public Dictionary<string, JSONValue> Val { get; set; } = val;
public override JSONValue this[string s] { get => Val[s]; }
public override JSONValue this[int i] { get => throw new IndexOutOfRangeException("Type JSONArray cannot be indexed via int"); }
}
using System.Text;
namespace JSON.Types;

abstract class JSONValue {
public abstract JSONValue this[string s] { get; }
public abstract JSONValue this[int i] { get; }
}

class JSONNum(int val) : JSONValue {
public int Val { get; set; } = val;
public override JSONValue this[string s] { get => throw new IndexOutOfRangeException("Cannot index type JSONNum"); }
public override JSONValue this[int i] { get => throw new IndexOutOfRangeException("Cannot index type JSONNum"); }
}

class JSONString(string val) : JSONValue {
public string Val { get; set; } = val;
public override JSONValue this[string s] { get => throw new IndexOutOfRangeException("Cannot index type JSONString"); }
public override JSONValue this[int i] { get => throw new IndexOutOfRangeException("Cannot index type JSONString"); }
}

class JSONObject(Dictionary<string, JSONValue> val) : JSONValue {
public Dictionary<string, JSONValue> Val { get; set; } = val;
public override JSONValue this[string s] { get => Val[s]; }
public override JSONValue this[int i] { get => throw new IndexOutOfRangeException("Type JSONArray cannot be indexed via int"); }
}
And want to define a class like:
using JSON.Types;

namespace JSON.Display;

class PrettyPrint() {
public static void Print(JSONValue v) {}

public static void PrintValue(string prefix, JSONNum v) {}
public static void PrintValue(string prefix, JSONString v) {}
public static void PrintValue(string prefix, JSONBool v) {}
public static void PrintValue(string prefix, JSONNull v) {}
public static void PrintValue(string prefix, JSONArray v) {}
public static void PrintValue(string prefix, JSONObject v) {}
}
using JSON.Types;

namespace JSON.Display;

class PrettyPrint() {
public static void Print(JSONValue v) {}

public static void PrintValue(string prefix, JSONNum v) {}
public static void PrintValue(string prefix, JSONString v) {}
public static void PrintValue(string prefix, JSONBool v) {}
public static void PrintValue(string prefix, JSONNull v) {}
public static void PrintValue(string prefix, JSONArray v) {}
public static void PrintValue(string prefix, JSONObject v) {}
}
How should I extend the functionality of the JSON types. I had a ToString() method, but it didnt deal with indents and single responsibility principle.
11 Replies
olayk
olayk2mo ago
I considered a bunch of if statements and casting the JSONValue to its subclasses, but this didnt seem good I have tried reading how this is implemented in std but it was a lot more complicated to understand
Becquerel
Becquerel2mo ago
this seems potentially like a case for interfaces? i.e. declare an IPrettyPrintable interface and implement it on JSONNUm, JSONString, JSONBool etc that way the relevant code for these types are kept with the rest of their functionality and you can just do myValue.PrettyPrint() if you implement it on the base JSONValue type or what have you
olayk
olayk2mo ago
would I be able to separate the printing part into a diffent namespace with this? ideally i would likee to be able to choose whether i care about the printing or not
Becquerel
Becquerel2mo ago
the implementation code for the interface would be in the same classes, and therefore namespaces, as the JSON types if you don't care about the printing you could not implement the interface for one of the classes i'm not sure how namespaces relate to that though
olayk
olayk2mo ago
@Becquerel (ping on reply please) this is what I ended up with but I'm not sure if it is good code:
class PrettyPrinter() {
public static void PrintValue(JSONValue v) {
PrintValue("", v);
}

public static void PrintValue(string prefix, JSONValue v) {
if (v.GetType() == typeof(JSONNum)) {
PrintValue(prefix, (JSONNum) v);
}
else if (v.GetType() == typeof(JSONString)) {
PrintValue(prefix, (JSONString) v);
}
else if (v.GetType() == typeof(JSONBool)) {
PrintValue(prefix, (JSONBool) v);
}
else if (v.GetType() == typeof(JSONNull)) {
PrintValue(prefix, (JSONNull) v);
}
else if (v.GetType() == typeof(JSONArray)) {
PrintValue(prefix, (JSONArray) v);
}
else if (v.GetType() == typeof(JSONObject)) {
PrintValue(prefix, (JSONObject) v);
}
}

public static void PrintValue(string _, JSONNum v) {
Console.Write($"{v.Val}");
}

public static void PrintValue(string _, JSONString v) {
Console.Write($"'{v.Val}'");
}
public static void PrintValue(string _, JSONBool v) {
Console.Write($"{v.Val}");
}
public static void PrintValue(string _, JSONNull v) {
Console.Write($"null");
}

public static void PrintValue(string prefix, JSONArray v) {
Console.Write("[");
foreach (var o in v.Val) {
Console.Write($"\n{prefix} ");
PrintValue(prefix + " ", o);
}
Console.Write($"\n{prefix}]");
}

public static void PrintValue(string prefix, JSONObject v) {
Console.Write("{");
foreach (var (key, o) in v.Val) {
Console.Write($"\n{prefix} {key} : ");
PrintValue(prefix + " ", o);
Console.Write(", ");
}
Console.Write("\n" + prefix + " }");
}
}
class PrettyPrinter() {
public static void PrintValue(JSONValue v) {
PrintValue("", v);
}

public static void PrintValue(string prefix, JSONValue v) {
if (v.GetType() == typeof(JSONNum)) {
PrintValue(prefix, (JSONNum) v);
}
else if (v.GetType() == typeof(JSONString)) {
PrintValue(prefix, (JSONString) v);
}
else if (v.GetType() == typeof(JSONBool)) {
PrintValue(prefix, (JSONBool) v);
}
else if (v.GetType() == typeof(JSONNull)) {
PrintValue(prefix, (JSONNull) v);
}
else if (v.GetType() == typeof(JSONArray)) {
PrintValue(prefix, (JSONArray) v);
}
else if (v.GetType() == typeof(JSONObject)) {
PrintValue(prefix, (JSONObject) v);
}
}

public static void PrintValue(string _, JSONNum v) {
Console.Write($"{v.Val}");
}

public static void PrintValue(string _, JSONString v) {
Console.Write($"'{v.Val}'");
}
public static void PrintValue(string _, JSONBool v) {
Console.Write($"{v.Val}");
}
public static void PrintValue(string _, JSONNull v) {
Console.Write($"null");
}

public static void PrintValue(string prefix, JSONArray v) {
Console.Write("[");
foreach (var o in v.Val) {
Console.Write($"\n{prefix} ");
PrintValue(prefix + " ", o);
}
Console.Write($"\n{prefix}]");
}

public static void PrintValue(string prefix, JSONObject v) {
Console.Write("{");
foreach (var (key, o) in v.Val) {
Console.Write($"\n{prefix} {key} : ");
PrintValue(prefix + " ", o);
Console.Write(", ");
}
Console.Write("\n" + prefix + " }");
}
}
i couldn't think of a way to do it apart from this unless i wrote the code for printing inside the JSON.Types i really want the printing of JSON to be separate to its representation
Parsed with no errors!
{
first_name : 'John',
last_name : 'Smith',
is_alive : True,
age : 27,
address : {
street_address : '21 2nd Street',
city : 'New York',
state : 'NY',
postal_code : '10021-3100',
},
phone_numbers : [
{
type : 'home',
number : '212 555-1234',
}
{
type : 'office',
number : '646 555-4567',
}
],
children : [
'Catherine'
'Thomas'
'Trevor'
],
spouse : null,
}
Parsed with no errors!
{
first_name : 'John',
last_name : 'Smith',
is_alive : True,
age : 27,
address : {
street_address : '21 2nd Street',
city : 'New York',
state : 'NY',
postal_code : '10021-3100',
},
phone_numbers : [
{
type : 'home',
number : '212 555-1234',
}
{
type : 'office',
number : '646 555-4567',
}
],
children : [
'Catherine'
'Thomas'
'Trevor'
],
spouse : null,
}
this is what the output looks like for an example jsonValuee another reason i want the printing to be separate is because i can then do a toFile kind of print that prints it out in bytes, with no whitespace is there a better way to do this using some OOP design pattern or does it need the dynamic dispatch based on the types
Becquerel
Becquerel2mo ago
that kind of separation is a valid concern and something to look into... i would look into the visitor pattern, perhaps a lesser-used pattern but i've seen it referenced as being particularly useful for parsing or consuming syntax-tree-like things
olayk
olayk2mo ago
i think that is exactly what i was looking for thank you i would just need to define an accept method in the JSONValue abstract class that accepts a PrettyPrinterVisitor and then define the prettyprintervisitor with the exact same methods that i have here and i could avoid the if statements for the type i will have a go at this tomorrow, and let you know how it turns out this is the sort of pattern that i dont think i could have come up with myself thank you @Becquerel (ping on reply please) ive just read some articles slating the visitor pattern, but its surely still better than dynamic dispatch? 😢
Becquerel
Becquerel2mo ago
if we're using the same definition of dynamic dispatch, i really wouldn't worry about it in c# since this is a GC language to begin with if performance is your concern
olayk
olayk2mo ago
I’m not too sure about the definitions but I meant it as dynamically choosing which func to call at runtime based on these type checks It seemed like bad code, but functionally I expect to pattern match on a type I tried using a switch but encountered some errors I was thinking something like data JsonVal = Num | String | Bool | Null | Array | Object And then pattern matching on these, where the compiler knows the exhaustive list of patterns it can be Defining a function to perform on each
Becquerel
Becquerel2mo ago
oh i see. in c# you don't really do that with pattern matching so much as you use inheritance and subtyping which also counts as dynamic dispatch because of vtable lookups etc. etc. that's what i was getting at earlier when i was talking about interfaces you work against variables of the IPrettyPrint type, and at runtime you dynamically get the implementation appropriate for the real type that kind of style would be very common and idiomatic in c# your style here sounds like something more common in rust, haskell or similar languages c# has functional features but its legacy and core is still OOP
olayk
olayk2mo ago
Yeah I’ve been coding a lot in Haskell recently What would this look like? Without the visitor pattern I can’t see how to implement the dynamic dispatch with interfaces I’m not too good at OOP compared to more functional code at the moment I was kinda expecting the dispatch to work so long as I implemented a PrintValue for each of the JSONValue subclasses but this isn’t the case I believe that the vtable type code would require implementing a function for each subclass which it then chooses the correct one? I imagine this to be like defining Print on each JSONValue subclass Again, not sure about this