Home > other >  Big configuration object as a paremeter in Swift?
Big configuration object as a paremeter in Swift?

Time:04-25

In JavaScript/TypeScript we have this pattern that is often used in libraries where you have one optional parameter as the last function argument which defaults to some default configuration. It looks like the following:

const smth = someLibrary("requiredParameter", {
  someOptionalProperty: true,
  // ...other options available, but not required to be provided here.
});

On the library side it would look something like this:

export const someLibrary = (requiredParameter: string, options?: Options) => {
  const opts = { ...defaultOptions, ...options };

  // Here `opts` will have the default properties merged with the ones that you provided.
}

Now, in Swift, from what my 2-3 year experience has told me, if you want to do something similar you have to make a struct with all the configuration parameters and then mutate the defaults after instantiating the struct. This would look something like the following:

struct Options {
  var option1: Double = 2.0
  var option2: Int = 1
  // ...etc
}

public func someLibrary(_ requiredParameter: String, options: Options?) {
  // ...
}

let opts = Options()
opts.someParameter = "Overriding the values here"

let result = someLibrary("requiredParameter", options: )

The problem

There are a couple of problems with the Swift implementation that JavaScript does very well:

  1. Default options override, but just the fields that were changed - I haven't been able to replicate this in swift with a solution that is at least close in elegancy.
  2. Partial override of the options - the approach in Swift that I was using is to have an initializer for the struct with all optional parameters, but the initializers become huge for big configurations.

The Question

Now the question is - how close can I get to the JavaScript version using Swift?

Maybe there's a pattern or a function that I'm missing that would do that for me?

The goal for me is to not have huge boilerplate to make my library customisable.

CodePudding user response:

Here a sample example :

struct Options {
    // option values are saved in private dictionary of Any
    private var options = [String:Any]()
    
    init(withOptions options: [String:Any]) {
        self.options = options
    }
    
    // Get int option
    subscript(intOption: String) -> Int {
        return options[intOption] as? Int ?? 0
    }

    // Get string option
    subscript(stringOption: String) -> String {
        return options[stringOption] as? String ?? ""
    }

    // Get float option
    subscript(floatOption: String) -> Float {
        return options[floatOption] as? Float ?? 0.0
    }
    
    // generic option but return an optional
    subscript<T>(withName: String) -> T? {
        return options[withName] as? T
    }
}

let options: Options = Options(withOptions: ["option1": 1,
                                             "option2": "string 2",
                                             "option3": Float(3.0), // set type if you want correct value
                                             "option4": 4.0])

let o1: Int = options["option1"]
let o2: String = options["option2"]
let o3: Float = options["option3"]
let o4: Double? = options["option4"]

This is a base that may help you, Here the subscript are an nice way go get values.

EDIT : added non typed access EDIT 2 : added Float for type accuracy

CodePudding user response:

One way to solve this is to let the caller configure the options in a closure, for this to work smoothly it is better if Options is changed to be a class

class Options {
    var option1: Double = 2.0
    var option2: Int = 1
    var option3: String = "abc"
}

Then we can change the signature of the library function to

func someLibrary(_ requiredParameter: String, 
                 options: ((Options) -> Void)? = nil)

and then in the body of the function call the closure

let libraryOptions = Options() //Holds all default values
if let options = options {
    options(libraryOptions)
} 

// use libraryOptions in code...

This can then be used in the client like this

someLibrary("requiredParameter") { options in
    options.option1 = 4.2
}

Old answer

One solution is to create an init for Options that takes default values

struct Options {
    var option1: Double
    var option2: Int
    var option3: String

    init(option1: Double = 2.0, option2: Int = 1, option3: String = "abc") {
        self.option1 = option1
        self.option2 = option2
        self.option3 = option3
    }
}

Then you get a use case that is similar to your JS example

let options = Options(option1: 4.2)
someLibrary("requiredParameter", options: options)
  • Related