reflectionchapel

Is a "setField()" like routine available in the Reflection module?


I have recently come across the Reflection module (in the standard library) below:

https://chapel-lang.org/docs/modules/standard/Reflection.html

I imagined that this module may be useful for creating a generic I/O library that reads in an input file (e.g. TOML) and automatically fills all the fields of class/record variables by scanning the field names and matching them with the contents of the input file. However, the Reflection module seems to provide only getField(), which returns field values as rvalue. So currently, is it not possible to do something like setField() to set field values via its name or index? FYI, my interest is whether something like below could be achieved via the Reflection module:

In the case of Swift: https://github.com/LebJe/TOMLKit

let toml = """
string = "Hello, World!"
int = 523053
double = 3250.34
"""

struct Test: Codable {
    let string: String
    let int: Int
    let double: Double
}
...
let test = Test(string: "Goodbye, World!", int: 24598, double: 18.247)

let encodedTest: TOMLTable = try TOMLEncoder().encode(test)
let decodedTest = try TOMLDecoder().decode(Test.self, from: encodedTest)

print("Encoded Test: \n\(encodedTest)\n")
print("Decoded Test: \(decodedTest)")

--> Result:

Encoded Test:
double = 18.247
int = 24598
string = 'Goodbye, World!'

Decoded Test: Test(string: "Goodbye, World!", int: 24598, double: 18.247)

and in the case of Rust : https://docs.rs/toml/latest/toml/

A similar example may be the "namelist" feature in Fortran, which can read in a user-defined type variable directly from an input file, for example, as follows:

!! test.f90
program main
    implicit none
    type Foo
        integer :: num
        real :: val
    endtype

    type(Foo) :: f
    namelist /myinp/ f

    open( 10, file="test.inp", status="old" )
    read( 10, nml=myinp )
    close( 10 )

    print *, "f % num = ", f % num
    print *, "f % val = ", f % val
end

!! test.inp
&myinp
  f%num = 100
  f%val = 1.23
/

$ gfortran test.f90 && ./a.out
 f % num =          100
 f % val =    1.23000002  

Solution

  • I'll get to your question about Reflection in a moment, but I first wanted to bring your attention to the Serializer and Deserializer features provided by the IO module.

    Here is a basic overview: https://chapel-lang.org/docs/modules/standard/IO.html#the-serialize-and-deserialize-methods

    And here's a link to more in-depth documentation, suitable for writing your own [De]Serializer or adding custom [de]serialization to a type: https://chapel-lang.org/docs/technotes/ioSerializers.html

    Here's an example using the JSON module:

    use IO, JSON;
    
    record Point {
      var x : int;
      var y : int;
    }
    
    proc main() {
      var temp = openTempFile();
      var orig = new Point(5, 10);
    
      // Write 'orig' in JSON to our in-memory file
      {
        var wr = temp.writer(locking=false, serializer=new jsonSerializer());
        wr.write(orig);
      }
    
      // Read the temp file's raw data to prove it wrote JSON
      {
        const data = temp.reader(locking=false).readAll(string);
        writeln("Wrote: ", data);
      }
    
      // Create a 'fileReader' configured to read JSON, and read the Point back in
      var rd = temp.reader(locking=false, deserializer=new jsonDeserializer());
      var pt = rd.read(Point);
      writeln("Read: ", pt);
    }
    

    This program prints:

    Wrote: {"x":5, "y":10}
    Read: (x = 5, y = 10)
    

    The Reflection module supports getFieldRef (see https://chapel-lang.org/docs/main/modules/standard/Reflection.html#Reflection.getFieldRef), which returns a mutable reference. This allows you to assign values to the field. Do also note that getFieldRef is currently unstable in the 2.0 release (most recent at the time of this comment).

    The following is an example program demonstrating how you could use getFieldRef to programmatically read generic types, in the event you or others find [De]Serializers to be insufficient.

    use IO, Reflection;
    
    // for 'isParam', 'isType'
    use Types;
    
    record A {
      var x : int;
      var y : string;
    }
    
    record B {
      var i : int;
      var j : int;
      var k : int;
    }
    
    record C {
      type T;
      var x : T;
    }
    
    proc readGeneric(reader : fileReader(?), ref arg: ?) {
      param numFields = getNumFields(arg.type);
    
      for param i in 0..<numFields {
        // 'type' and 'param' fields must be known at compilation time
        if !(isParam(getField(arg, i)) || isType(getField(arg, i))) {
          param name : string = getFieldName(arg.type, i);
          ref fieldRef = getFieldRef(arg, i);
    
          // %? = read according to type of 'fieldRef'
          reader.readf(name + ": %?\n", fieldRef);
        }
      }
    }
    
    proc test(type T, data: string) {
      var temp = openTempFile();
      {
        // In brackets to force temporary writer to flush at end of scope
        temp.writer(locking=false).write(data);
      }
    
      var r = temp.reader(locking=false);
      var val : T;
      readGeneric(r, val);
      writeln("Read type '" + T:string + "': ", val);
    }
    
    proc main() {
      const AData =
    '''x: 5
    y: "hello"''';
      test(A, AData);
    
      const BData =
    '''i: 1
    j: 2
    k: 3''';
      test(B, BData);
    
      // Examples of generic types
      const CData_int = '''x: 5''';
      test(C(int), CData_int);
    
      const CData_string = '''x: "five"''';
      test(C(string), CData_string);
    }
    

    This program prints:

    Read type 'A': (x = 5, y = "hello")
    Read type 'B': (i = 1, j = 2, k = 3)
    Read type 'C(int(64))': (x = 5)
    Read type 'C(string)': (x = "five")