Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Would a function family like BindNofVariadic() be useful? | Chaining w/ inheritance #38

Closed
a-lipson opened this issue Sep 8, 2023 · 4 comments

Comments

@a-lipson
Copy link
Contributor

a-lipson commented Sep 8, 2023

Please forgive me for creating this issue for creating this issue if this functionality is already possible with a composition of extant functions.

Nonetheless, let me describe the scenario I am working with.

I have a function from a library that takes a single variadic input. I have a pipeline of functions that eventually needs to put an item into this variadic function.

The current approach that I am taking is to pipe up to the last function and store that output, then to pass the output to the the library's function.

I have also taken a look at using the Unvariadic0 function, but I think the method above is simpler.

This leads me to wonder if functions that would take functions with variadic arguments and produce functions with a finite set of arguments would be helpful.

While looking for a way to accomplish this, I found the bind and ignore functions which are similar, but I don't fully understand them. Hence why I imagined that there probably exists a way to accomplish what I was trying to accomplish, yet I cannot see it.

I might implement the BindNofVariadic function like this,

func Bind1ofVariadic[T, R any](f func(...T) R) func(T) R {
    return func(t T) R {
        return f(t)
    }
}

Then for two arguments,

func Bind2ofVariadic[T, R any](f func(...T1) R) func(T, T) R {
    return func(t1, t2 T) R {
        return f(t1, t2)
    }
}

One potential consideration would be the case where a function has its variadic argument in the second, third, fourth, etc. position. As is the case with the UnvariadicN family of functions.

The minimal differences between Bind1..., Bind2... and BindN... suggest to me now that this functionality might be accomplished by some form of UnvariadicN and then some sort of indexing of the resulting slice.
For example, if there were a family of slice builder functions from some data, like FromN, then one could go from the single value to a From1 to a Unvariadic0 on some function f. However, I think this might be just another version of the same proposed solution.

Or perhaps this problem is indicative of some inherent design issue, rather than reason for a new function.


Another question that I have is about how chaining with inherited types works.

Here are two examples that I created to try to better understand.

type MyString string

// need to define a cast method
func (m MyString) String() string { return string(m) }

func TestCastingInPipeline() {

	var text MyString = "hi"

	uppercase := strings.ToUpper
	cast := MyString.String

	out1 := F.Pipe2(text, cast, uppercase)

	out2a := uppercase(cast(text))
	out2b := uppercase(string(text)) // works without cast method

	// assert.Equal(t, out1, out2a, out2b)
}

and

type Thing interface {
	Do() int
}

type MyType struct{}

// make MyType implement Thing
func (m MyType) Do() int { return 1 }

func ThingAction(thing Thing) int { return thing.Do() }

func TestInheritancePipeline() {

	example := MyType{}

	// MyType implements Thing but type of MyType != type of Thing
	out1 := F.Pipe1(example, ThingAction)

	// MyType implements Thing
	out2 := ThingAction(example)

	// assert.Equal(t, out1, out2)

}

Aside from this, I am enjoying trying to implement functional design patterns in my go project.
I am following the tutorials that you have linked (both Ryan Lee's blog and Professor Frisby's Mostly Adequate Guide to Functional Programming)
In addition, I am trying to learn Haskell and Category Theory (mainly following Bartosz Milewski's lectures on YouTube at the moment) to have a better understanding of the functional concepts before employing them in go.

Thank you very much for this resource and assistance!

@CarstenLeue
Copy link
Collaborator

CarstenLeue commented Sep 11, 2023

Hi @a-lipson

let's me comment on the second aspect, type problem first.
I have no good solution, I'd be grateful for any idea. From my perspective the issue is that in go there is no way to define covariant data types, all types are invariant. For your Thing problem you use the F.Pipe1 method which is defined as:

func Pipe1[F1 ~func(T0) T1, T0, T1 any](t0 T0, f1 F1) T1 {
  • T0 is MyType
  • T1 is int
  • F1 is func(MyType)int
  • ThingAction is func(Thing) int

Since MyType is not identical to Thing this does not work. In fact the signature of Pipe1 is too strict, what I would have liked to express is that F1 takes an argument of a type that is assignable from T0.

But as far as I know this is not possible in go, it would have to be something along these lines:

func Pipe1[F1 ~func(B0) T1, B0 any, T0 ~B0, T1 any](t0 T0, f1 F1) T1 {

trying to express that the starting type t0 T0 has B0 as its base type and this is the type that the function expects. In Java you would e.g. use T0 extends B0 for this purpose.

But this is unsupported. https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#no-way-to-express-convertibility

The only workaround I am aware of is to write an explicit type conversion function and add that to the pipe. Not nice, but a mitigation.

func (m MyType) AsThing() Thing { return m }

out1 := F.Pipe2(example, MyType.AsThing, ThingAction)

The direct invocation ThingAction(example) does support covariant type support though. 🤷

You might ask if such an implicit type conversion could be a helper function in the library. I was actually not able to find a good solution, the signature would have to be something like:

func ToType[A any, B ~A](b B) A {
  return b
}

But that is not possible. We could do

func ToType[A, B any(b B) A {
  return any(b).(A)
}

but then you lose type safety and the pre-knowlege that A and B are related.

So my best guess are custom type conversions of the form func (m MyType) AsThing() Thing { return m }

Do you have any ideas?

@CarstenLeue
Copy link
Collaborator

On variadic function support. So far this is the train of thoughts:

  • variadic arguments are always the last arguments in a function signature. So the set of UnvariadicN methods takes a function with N non-variadic arguments and adds a N+1st that represents the array(slice) of the variadic arguments
  • the set of BindNofM functions is meant to reduce the arity of a Mary function by pinning down (binding) the some of the parameters to static values
  • the composition functions PipeN and FlowN work on unary functions, i.e. one input one output
  • if you want to use a function taking more than one parameter in a composition, then the idea is to wrap the parameters into a single "wrapper" and pass that wrapper as a single argument.
    • if all parameters are of the same type you can use an array and A.From
    • if the parameters are of different types, use a tutple and T.MakeTupleN and TupledN/UntupledN

A simple usecase using Unvariadic0 would be

func fromLibrary(data ...string) string {
	return strings.Join(data, "-")
}

func TestUnvariadic(t *testing.T) {

	res := Pipe1(
		[]string{"A", "B"},
		Unvariadic0(fromLibrary),
	)

	assert.Equal(t, "A-B", res)
}

From a naming perspective the naming func Bind1ofVariadic[T, R any](f func(...T) R) func(T) R { does not really fit the naming of the existing Bind functions, since for the existing ones the set of numbers after Bind identities the indexes of the parameters to pin down. In Bind1ofVariadic the 1 is rather the arity of the resulting function.

@CarstenLeue
Copy link
Collaborator

How about this:

  • we add a function similar to UntupledN but for slices, e.g.
func Unsliced2[F ~func([]T) R, T, R any](f F) func(T, T) R {
	return func(t1 T, t2 T) R {
		return f([]T{t1, t2})
	}
}

and then we can use this in conjunction with Unvariadic0 like so:

func fromLibrary(data ...string) string {
	return strings.Join(data, "-")
}

func Unsliced2[F ~func([]T) R, T, R any](f F) func(T, T) R {
	return func(t1 T, t2 T) R {
		return f([]T{t1, t2})
	}
}

func TestVariadicArity(t *testing.T) {

	f := Unsliced2(Unvariadic0(fromLibrary))

	res := f("A", "B")
	assert.Equal(t, "A-B", res)
}

@a-lipson
Copy link
Contributor Author

Hi @CarstenLeue, thank you very much for your explanations.

I did not realize that the inheritance problem that I was encountering was the same as the variant/covariant data types. I am still learning.

I think I have a better understanding of the problem now, and I can certainly see how it's perhaps not possible to do a convertibility check and the only workaround appears to be some sort of specific casting method or a loss of type-safety.

I would prefer a boilerplate casting method as opposed to a loss of type safety.


With regards to the variadics, as of my current understanding, it seems that UnslicedN would be a helpful series of functions.
However, I think that I need to learn more about currying and composition of curried functions in order to find different approaches to the same issue. So, I would defer to your much much more experienced opinion if such a function family would be both useful, but, more importantly I'd imagine, conducive to writing good code.

Additionally, I will try testing composition of functions with multiple input parameters using the tuple functions.

Once again, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants