Another FFI related question From JS --> PS

#1

So there is a function in an npm module which takes 2 inputs (String, any) and gives out an array of any type. How would I go about defining the FFI of this function?

The function referred to comes from https://repetere.github.io/modelscript/.

exports.loadCSV = modelscript.csv.loadCSV()

function signature --> loadCSV(filepath: string, options?: any): any[]

#2

You can represent multi-parameter functions using Fn2, Fn3, etc. This function appears to have side effects though, so you’ll want EffectFn2 String OpaqueType1 (Array OpaqueType2). If promises are involved you’ll want to convert them to Affs. There’s a library for that.

1 Like
#3

Thanks for the prompt answer!!

Fn2, Fn3 are part of Data.Function.Uncurried but I cannot find OpaqueType1 . Can you please specify which library is it a part of because I can’t find it on pursuit. Thanks again :grin:.

#4

They probably meant to define your own type. An alternative (which I use all the time) is to use the Foreign type: https://pursuit.purescript.org/packages/purescript-foreign/5.0.0 and https://pursuit.purescript.org/packages/purescript-foreign-generic/10.0.0

foreign-generic in particular has a lot of methods to deal with untyped JS values.

1 Like
#5

I’m trying to do it using foreign import loadCSV :: String -> Foreign

I’m using purescript-foreign library. I’m still not sure how to use it though. But can you please help me out.

For starters I’m sure the type should be loadCSV :: String -> Foreign -> Array Foreign. But I’m not sure what functions to apply to get the types of the foreign.

It would be really helpful of you if you could help me out here. Thanks!!

#6

Those are the encoding functions in foreign-generic: https://pursuit.purescript.org/packages/purescript-foreign-generic/10.0.0/docs/Foreign.Generic.Class#t:Encode

in the case of function arguments. For output, to get it from Foreign to something else you need to use a decoder: https://pursuit.purescript.org/packages/purescript-foreign-generic/10.0.0/docs/Foreign.Generic.Class#t:Decode

#7

So I tried using decode, I used foreign import loadCSV :: Foreign . And then I ran decode loadCSV on the repl.

> decode loadCSV
Error found:
in module $PSCI
at <internal>:0:0 - 0:0 (line 0, column 0 - line 0, column 0)

  No type class instance was found for
                                                                    
    Data.Show.Show (ExceptT (NonEmptyList ForeignError) Identity t2)
                                                                    
  The instance head contains unknown type variables. Consider adding a type annotation.

while applying a function eval
  of type Eval t1 => t1 -> Effect Unit
  to argument it
while checking that expression eval it
  has type Effect t0
in value declaration $main

where t0 is an unknown type
      t1 is an unknown type
      t2 is an unknown type

See https://github.com/purescript/documentation/blob/master/errors/NoInstanceFound.md for more information,
or to contribute content related to this error.

Sorry to be a noob, it would really clear things up.

#8

This error message means that the repl doesn’t know what type you want to decode loadCSV as. The decoder which gets used when you decode a Foreign value is selected at compile-time, so the compiler needs to know what type you’re expecting. In a proper source file this can usually be inferred, but in the repl you’ll often need to provide an annotation. You can do so like this:

> (decode loadCSV :: Except (NonEmptyList ForeignError) MyType)

where MyType is whatever you expect to be able to decode the foreign data as.

1 Like
#9

I couldn’t guess the type, probably a lot of time was burned into it

#10

A lot of the time when something uses any it doesn’t actually mean that. In this case I don’t think you need Foreign. Presumably the options object only has a few possible keys it understands, and each of those will be expected to be a specific type? If so the purescript-options library can help with this. Also for the return value, the keys will presumably be the headers, and the values will presumably all be strings? In that case you can use Foreign.Object.Object String.

Giving typed interfaces to APIs which weren’t designed with any types in mind is often difficult, so it’s fine to start off by only supporting the bare minimum subset of the API which you actually need.

1 Like
#11

Thanks for the help, I’ll look into purescript-options and also try out using String. I know it’s difficult and probably would take time. I’m hopeful that I’ll eventually get over this hump.

Also there is an example I don’t know how to untangle it in terms of PS.

import { default as jsk } from 'modelscript';
let dataset;

//In JavaScript, by default most I/O Operations are asynchronous, see the notes section for more
ms.loadCSV('/some/file/path.csv')
  .then(csvData=>{
    dataset = new ms.DataSet(csvData);
    console.log({csvData});
    /* csvData [{
      'Country': 'Brazil',
      'Age': '44',
      'Salary': '72000',
      'Purchased': 'N',
    },
    ...
    {
      'Country': 'Mexico',
      'Age': '27',
      'Salary': '48000',
      'Purchased': 'Yes',
    }] */
  })
  .catch(console.error);

// or from URL
ms.loadCSV('https://example.com/some/file/path.csv')

I don’t know if it helps, but there is a DataSet class defined as

DataSet [Class: DataSet]: { //class for manipulating an array of objects (typically from CSV data)
    columnMatrix(vectors), //returns a matrix of values by combining column arrays into a matrix
    columnArray(columnName, options), // - returns a new array of a selected column from an array of objects, can filter, scale and replace values
    columnReplace(columnName, options), // - returns a new array of a selected column from an array of objects and replaces empty values, encodes values and scales values
    columnScale(columnName, options), // - returns a new array of scaled values which can be reverse (descaled). The scaling transformations are stored on the DataSet
    columnDescale(columnName, options), // - Returns a new array of descaled values
    selectColumns(columns, options), //returns a list of objects with only selected columns as properties
    labelEncoder(columnName, options), // - returns a new array and label encodes a selected column
    labelDecode(columnName, options), // - returns a new array and decodes an encoded column back to the original array values
    oneHotEncoder(columnName, options), // - returns a new object of one hot encoded values
    columnMatrix(columnName, options), // - returns a matrix of values from multiple columns
    columnReducer(newColumnName, options), // - returns a new array of a selected column that is passed a reducer function, this is used to create new columns for aggregate statistics
    columnMerge(name, data), // - returns a new column that is merged onto the data set
    filterColumn(options), // - filtered rows of data,
    fitColumns(options), // - mutates data property of DataSet by replacing multiple columns in a single command
    static reverseColumnMatrix(options), // returns an array of objects by applying labels to matrix of columns
    static reverseColumnVector(options), // returns an array of objects by applying labels to column vector
  }

So there seems to be an asynchronous function call which then takes the output as form of promise and then uses the DataSet class to format the data or something I’m not completely sure of.

Source : https://github.com/repetere/modelscript/blob/master/docs/api.md

Also found the function’s source

export async function loadCSV(filepath, options) {
  if (validURL.isUri(filepath)) {
    return loadCSVURI(filepath, options);
  } else {
    return new Promise((resolve, reject) => {
      const csvData = [];
      const config = Object.assign({ checkType: true, }, options);
      csv(config).fromFile(filepath)
        .on('json', jsonObj => {
          csvData.push(jsonObj);
        })
        .on('error', err => {
          return reject(err);
        })
        .on('done', error => {
          if (error) {
            return reject(error);
          } else {
            return resolve(csvData);
          }
        });
    });
  }
}
#12

Would recommend to take a look at chapter 10. The Foreign Function Interface of the PureScript book. Especially this part 10.16 Working With Untyped Data

#13

Thanks for the link, the link tells how to decode and encode json. I’m not well versed with JS, can every output from a function call be converted to JSON?