vincent-pradeilles / swift-tips

A collection useful tips for the Swift language
MIT License
996 stars 94 forks source link

Configuration closures keypath #3

Open siideffect opened 4 years ago

siideffect commented 4 years ago

Hello Vincent, this repo is so useful if you are interested about keypaths. I have been digging into the argument for the past weeks, but i still cant master the topic as i would like to. Supposing you have a function to customize an object by using a closure:

func configure<T: UIView>(block: (T)->())-> T{
   let obj = T()
   block(obj)
   return obj
}

using it, will result in something like:

let label: UILabel = configure {
      $0.text = "Some"
      $0.backgroundColor = .white
   }

Although useful, i was wondering if keypaths could help making the syntax more concise. Would be possible to achieve something like this?

let label: UILabel = configure {
      \.text = "Some"
      \.backgroundColor = .white
}

07 Implementing the builder pattern with keypaths

This section is absolutely interesting, and is very close to my concept of clean and concise syntax.

vincent-pradeilles commented 4 years ago

Hi, yes you could achieve this with custom operators and function builders.

Here is the code for the function builder part :

(From https://twitter.com/v_pradeilles/status/1138555570980626432/)

D8z2P-hWwAEH7xn

Then, you just need to define a custom operator that returns a Configuration, with a key path as left hand side and the value as the right hand side, and that should do the trick 🙂

siideffect commented 4 years ago

Complex but beautiful! Although i did not get the 100% of the above code, your hint about defining a custom operator will be a good start. Thank you so much for the provided code.

astrokin commented 4 years ago

2020-04-09 20 51 40 do the sam but much clear and require less code

siideffect commented 4 years ago

Thank you @astrokin i have already used the example provided in #07 Implementing the builder pattern with keypaths And it looks really similar to the solution you proposed. Although your reply may be close to what i m trying to achieve, i m trying to achieve this specific syntax inside the closure

//let label = Configurator(UILabel){ this loook good
let label: UILabel = configure { 
      \.text = "Some" // this is the desired result
      \.backgroundColor = .white
}
astrokin commented 4 years ago

you can achieve it this way too https://gist.github.com/nicklockwood/9b4aac87e7f88c80e932ba3c843252df

siideffect commented 4 years ago

My actual implementation is almost identical to with function @astrokin Its good, but far from what i want: i am looking for something different, just to push my skills using keypaths, and in terms of readability, by my personal point of view. Thanks for your help!

siideffect commented 4 years ago

@vincent-pradeilles i have been usign the code you provided to achieve, for first, the DSL like syntax in order to be able to write

let aVar = UILabel{
     set(\.backgroundColor, to: .white)
     set(\.text, to: "Some")
     //and so on
}

However, this doesnt compile. It does until the instruction is one, but not with multi line assignments, complaining is not possible to infer the T type. I have checked the syntax hundreds of time, and i understood most of the code which is being used. Using the convenience init, you initialize the UILabel with a builder closure. This closure, is expected to return a single Configuration object: using the with function concatenation, however, is like writing additional instructions not needed by compiler, as it works as expected with a single line assignment.

As workaround to make compiler happy, i ve been using the following syntax:

let label = UILabel{
   set(\.backgroundColor, to: .yellow)
   .set(\.text, to: "Some")
   .set(\.frame, to: CGRect(x: 100, y: 0, width: 100, height: 200))
   .set(\.textAlignment, to: .center)
   //2 set functions, 1 declared inside struct Configuration<T: AnyObject>
   // which uses the combined function. The problem? DRY 😖
}
///OR
let label = UILabel {
   ConfigurationBuilder.buildBlock(
      set(\.backgroundColor, to: .white),
      set(\.text, to: "Some")
   )
}

but as you can see, it required explicit call to buildBlock function and comma separation between instructions. Im pretty sure this is the purpose of @_functionBuilder, which is identical to your code:

@_functionBuilder
struct ConfigurationBuilder{
   static func buildBlock<T>(_ children: Configuration<T>...) -> Configuration<T>{
      children.reduce(.empty, {$0.combined(with: $1)})
   }
}

extension UILabel{
   convenience init(@ConfigurationBuilder  builder: ()-> Configuration<UILabel>){
      self.init()
      builder().configurator(self)
   }
}

What is wrong?

vincent-pradeilles commented 4 years ago

That's indeed very weird! The code I gave you was from the Xcode 11 beta, and now it seems that on current versions it's not working anymore.

For the moment I don't have an explanation as to why it no longer works.