Functional programming polymorhism [duplicate]

2020-08-09 04:19发布

问题:

I wonder how to create polymorphism in functional programming. Lets take as example form class with different type of inputs Sample polymorh in JS es6:

class Form {
    constructor(fields){
      this.fields = fields;
    } 
    validate() {
       let err = null;
       for(let f of this.fields) {
           if (! field.validate()){
               return field.getErr();
           }
       }
    }
}

class TextField {
   isNotEmpty() {
      return this.value != ''
   } 
   validate() {
      return this.isNotEmpty()
   }

}
class SelectField {
   validate() {
       if (! inArray(this.value, this.options) {
           return 'value is not in the available options'
       }
   }
}

The code is for demonstration purposes only, dont look the syntax or the correct behavior.

I wonder how to create similar polymorhism in FP. In Haskell you can use pattern matching, but how to do it in javascript?

Some say that i can implement different functions with if statements:

if (field.type == 'text')...
else if (field.type == 'select')...

But this is bad because i should write all logic in one function. If i want to add new type of field (MultipleSelect for example) i should change the code inside the common funciton.

Is there better alternative in javascript?

回答1:

The only approach I can think of, from what I got from the OP's provided example code, was a mapping of the currently processed form-field to field-specific validation and error-handling functionality ... a working example code then looks like that ...

/**
 *  form and field type matching configuration tables/maps
 */

const validationTable = {
  isValidFieldType: {

    text:         isValidTextField,
    select:       isValidSelectField,
  //multiselect:  ...

    __invalid__:  returnInvalidFieldValue
  },
  validateForm:   validateForm
};

const createErrorTable = {
//form:           createFormError,
  field: {

    text:         createTextFieldError,
    select:       createSelectFieldError,
  //multiselect:  ...

    __invalid__:  handleCreateErrorException
  }
};


/**
 *  helping getter functionality
 *  - that either returns a valid field type that does
 *    match a key in `validationTable.isValidFieldType`
 *  - or that does return the '__invalid__' key as it's
 *    fallback value.
 */
function getValidationFieldType(field) {
  const fieldType = (field && field.type);
  return (

       (typeof fieldType === 'string')
    && (fieldType in validationTable.isValidFieldType)
    && fieldType

  ) || '__invalid__';
}


/**
 * main validation functionality
 * - that either returns `true` for an entirely valid form validation process
 * - or that does return a type specific/related validation error.
 */
function validateForm(type) {
  const tableOfIsValidFieldType = validationTable.isValidFieldType;
  let recentField;

  return Array.from(type.fields).every(field => {

    recentField = field;
    return tableOfIsValidFieldType[getValidationFieldType(field)](field);

  }) || createErrorTable.field[getValidationFieldType(recentField)](/*recentField*/);
}


/**
 *  validation and error handling specific helper functionality
 */

function isValidTextField(type) {
  return (type.value !== '');
}
function isValidSelectField(type) {
  return Array.from(type.options).includes(type.value);
}

function returnInvalidFieldValue() {
  return false;
}


function createTextFieldError(/*recentField*/) {
  return (new Error('an empty text field value is not allowed.'));
}
function createSelectFieldError(/*recentField*/) {
  return (new Error('the field value does not match the options'));
}

function handleCreateErrorException(/*recentField*/) {
  return (new Error('there is no matching validation and error handling for this field.'));
}


/**
 *  test
 */

const validForm = { fields: [{
  type:   'text',
  value:  'valid'
}, {
  type:   'select',
  value:  'valid',
  options: ['valid', 'select', 'field']
}] };

const invalidFormWithNullType = { fields: [{
  type:   'text',
  value:  'valid'
}, {
  type:   null,
  value:  'valid'
}, {
  type:   'select',
  value:  'valid',
  options: ['valid', 'select', 'field']
}] };

const invalidFormWithMissingType = { fields: [{
  value:  'valid'
}, {
  type:   'select',
  value:  'valid',
  options: ['valid', 'select', 'field']
}] };

const invalidFormWithEmptyTextValue = { fields: [{
  type:   'text',
  value:  ''
}, {
  type:   'select',
  value:  'valid',
  options: ['valid', 'select', 'field']
}] };

const invalidFormWithNonMatchingSelectValue = { fields: [{
  type:   'text',
  value:  'valid'
}, {
  type:   'select',
  value:  'invalid',
  options: ['valid', 'select', 'field']
}] };


console.log('validateForm(validForm) : ', validateForm(validForm));

console.log('validateForm(invalidFormWithNullType).message : ', validateForm(invalidFormWithNullType).message);
console.log('validateForm(invalidFormWithMissingType).message : ', validateForm(invalidFormWithMissingType).message);

console.log('validateForm(invalidFormWithEmptyTextValue).message : ', validateForm(invalidFormWithEmptyTextValue).message);
console.log('validateForm(invalidFormWithNonMatchingSelectValue).message : ', validateForm(invalidFormWithNonMatchingSelectValue).message);
.as-console-wrapper { max-height: 100%!important; top: 0; }

Note

This pretty much also fits to what @Bergi yesterday already did mention in one of his comments.

Notice that OOP and FP are orthogonal, you can easily combine them.

Here a function based solution meets two objects that have these functions assigned as their's data. A field from now will be treated as pure data by these functions (as shown with the example) but always has to feature an additional type property.