Skip to content

Commit

Permalink
#717 - Support partial application of functions.
Browse files Browse the repository at this point in the history
  • Loading branch information
sys27 committed Sep 10, 2023
1 parent 2b9001b commit dd04f2e
Show file tree
Hide file tree
Showing 22 changed files with 658 additions and 31 deletions.
1 change: 1 addition & 0 deletions docs/articles/change-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* [#706](https://github.com/sys27/xFunc/issues/706) - `CallExpression` uses incorrect scope for parameters evaluation ([#707](https://github.com/sys27/xFunc/issues/707)).
* [#708](https://github.com/sys27/xFunc/issues/708) - Remove unnecessary variables.
* [#710](https://github.com/sys27/xFunc/issues/710) - Add ability to execute expression from files ([#712](https://github.com/sys27/xFunc/pull/712)).
* [#717](https://github.com/sys27/xFunc/pull/717) - Support partial application of functions ([#718](https://github.com/sys27/xFunc/pull/718)).

## xFunc v4.3.0

Expand Down
2 changes: 1 addition & 1 deletion docs/articles/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var exp = new Pow(

### Execute methods

Let's assume you have `exp` from the [previous step](#parse-you-first-expression). Now you can evaluate it but because the expression has the `x` variable you need to provide a value for it.
Let's assume you have `exp` from the [previous step](#parse-your-first-expression). Now you can evaluate it but because the expression has the `x` variable you need to provide a value for it.

```csharp
var parameters = new ExpressionParameters
Expand Down
4 changes: 4 additions & 0 deletions xFunc.Maths/Analyzers/Analyzer{TResult,TContext}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ public virtual TResult Analyze(CallExpression exp, TContext context)
public virtual TResult Analyze(LambdaExpression exp, TContext context)
=> Analyze(exp as IExpression, context);

/// <inheritdoc />
public virtual TResult Analyze(Curry exp, TContext context)
=> Analyze(exp as IExpression, context);

/// <inheritdoc />
public virtual TResult Analyze(Variable exp, TContext context)
=> Analyze(exp as IExpression, context);
Expand Down
4 changes: 4 additions & 0 deletions xFunc.Maths/Analyzers/Analyzer{TResult}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ public virtual TResult Analyze(CallExpression exp)
public virtual TResult Analyze(LambdaExpression exp)
=> Analyze(exp as IExpression);

/// <inheritdoc />
public virtual TResult Analyze(Curry exp)
=> Analyze(exp as IExpression);

/// <inheritdoc />
public virtual TResult Analyze(Variable exp)
=> Analyze(exp as IExpression);
Expand Down
9 changes: 9 additions & 0 deletions xFunc.Maths/Analyzers/Formatters/CommonFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,15 @@ public virtual string Analyze(CallExpression exp)
public virtual string Analyze(LambdaExpression exp)
=> $"{exp.Lambda}";

/// <inheritdoc />
public virtual string Analyze(Curry exp)
{
if (exp.Parameters.Length > 0)
return $"curry({exp.Function}, {string.Join(", ", exp.Parameters)})";

return $"curry({exp.Function})";
}

/// <inheritdoc />
public virtual string Analyze(Variable exp) => exp.Name;

Expand Down
8 changes: 8 additions & 0 deletions xFunc.Maths/Analyzers/IAnalyzer{TResult,TContext}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ public interface IAnalyzer<out TResult, in TContext>
/// <returns>The result of analysis.</returns>
TResult Analyze(LambdaExpression exp, TContext context);

/// <summary>
/// Analyzes the specified expression.
/// </summary>
/// <param name="exp">The expression.</param>
/// <param name="context">The context.</param>
/// <returns>The result of analysis.</returns>
TResult Analyze(Curry exp, TContext context);

/// <summary>
/// Analyzes the specified expression.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions xFunc.Maths/Analyzers/IAnalyzer{TResult}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,13 @@ public interface IAnalyzer<out TResult>
/// <returns>The result of analysis.</returns>
TResult Analyze(LambdaExpression exp);

/// <summary>
/// Analyzes the specified expression.
/// </summary>
/// <param name="exp">The expression.</param>
/// <returns>The result of analysis.</returns>
TResult Analyze(Curry exp);

/// <summary>
/// Analyzes the specified expression.
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions xFunc.Maths/Analyzers/TypeAnalyzers/TypeAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,22 @@ public virtual ResultTypes Analyze(CallExpression exp)
public virtual ResultTypes Analyze(LambdaExpression exp)
=> CheckArgument(exp, ResultTypes.Function);

/// <inheritdoc />
public virtual ResultTypes Analyze(Curry exp)
{
if (exp is null)
throw new ArgumentNullException(nameof(exp));

if (exp.Parameters.Length == 0)
{
var functionResult = exp.Function.Analyze(this);
if (functionResult == ResultTypes.Function)
return ResultTypes.Function;
}

return ResultTypes.Undefined;
}

/// <inheritdoc />
public virtual ResultTypes Analyze(Variable exp)
=> CheckArgument(exp, ResultTypes.Undefined);
Expand Down
15 changes: 2 additions & 13 deletions xFunc.Maths/Expressions/CallExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,27 +71,16 @@ public override bool Equals(object? obj)
}

/// <inheritdoc />
/// <exception cref="NotSupportedException">The evaluation of this expression requires <see cref="ExpressionParameters"/>.</exception>
public object Execute()
=> throw new NotSupportedException();
=> Execute(null);

/// <inheritdoc />
public object Execute(ExpressionParameters? parameters)
{
if (parameters is null)
throw new ArgumentNullException(nameof(parameters));

if (Function.Execute(parameters) is not Lambda function)
throw new ResultIsNotSupportedException(this, Function);

var nestedScope = ExpressionParameters.CreateScoped(function.CapturedScope ?? parameters);
var zip = function.Parameters.Zip(Parameters, (parameter, expression) => (parameter, expression));
foreach (var (parameter, expression) in zip)
nestedScope[parameter] = new ParameterValue(expression.Execute(parameters));

var result = function.Call(nestedScope);
if (result is Lambda lambdaResult)
return lambdaResult.Capture(nestedScope);
var result = function.Call(Parameters, parameters);

return result;
}
Expand Down
183 changes: 183 additions & 0 deletions xFunc.Maths/Expressions/Curry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright (c) Dmytro Kyshchenko. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Immutable;
using System.Diagnostics;

namespace xFunc.Maths.Expressions;

/// <summary>
/// Represents the "curry" function which allows you to convert any lambda to the list of nested lambdas which you can partially apply.
/// </summary>
/// <remarks>
/// <para>This function takes an indefinite amount of arguments where:</para>
/// <list type="bullet">
/// <item>
/// <term>the first one is required and should always return a lambda.</term>
/// </item>
/// <item>
/// <term>all other arguments are optional and needed only when you want to apply some parameters immediately.</term>
/// </item>
/// </list>
/// <para>If the provided lambda takes 0 or 1 argument this function does nothing, it returns the same lambda without any modification.</para>
/// <para>If you provided enough parameters to just call a lambda, then this function works in the same way as <see cref="CallExpression"/>. It doesn't convert the provided lambda into the list of nested lambdas but instead just calls it directly.</para>
/// </remarks>
/// <example>
/// <para>Convert the lambda to the list of nested lambdas:</para>
/// <code>
/// f := (a, b, c) => a + b + c
/// p := curry(f) // `p` will contain (a) => (b) => (c) => a + b + c
/// </code>
/// <para>Partially apply the lambda:</para>
/// <code>
/// f := (a, b, c) => a + b + c
/// p := curry(f) // `p` will contain (a) => (b) => (c) => a + b + c
/// add1 := p(1) // partial application, you provided a value for the first parameter only
/// // so `p(1)` return another lambda which accepts the second parameter.
/// </code>
/// <para>Partially apply the lambda by using the `curry` function:</para>
/// <code>
/// f := (a, b, c) => a + b + c
/// add1 := curry(f, 1) // partial application, the result is equal to the previous example
/// // but it automatically convert the lambda to the list of nested lambdas
/// // and tries to apply all provided parameters
/// </code>
/// </example>
public class Curry : IExpression
{
/// <summary>
/// Initializes a new instance of the <see cref="Curry"/> class.
/// </summary>
/// <param name="function">The expression that returns a lambda.</param>
/// <exception cref="ArgumentNullException"><paramref name="function"/> is null.</exception>
public Curry(IExpression function)
: this(function, ImmutableArray<IExpression>.Empty)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="Curry"/> class.
/// </summary>
/// <param name="function">The expression that returns a lambda.</param>
/// <param name="parameters">The list of parameters.</param>
/// <exception cref="ArgumentNullException"><paramref name="function"/> is null.</exception>
public Curry(IExpression function, ImmutableArray<IExpression> parameters)
{
Function = function ?? throw new ArgumentNullException(nameof(function));
Parameters = parameters;
}

/// <summary>
/// Initializes a new instance of the <see cref="Curry"/> class.
/// </summary>
/// <param name="arguments">The arguments.</param>
/// <exception cref="ArgumentNullException"><paramref name="arguments"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">The amount of argument in the <paramref name="arguments"/> collection is less than 1.</exception>
internal Curry(ImmutableArray<IExpression> arguments)
{
Debug.Assert(arguments != null, "arguments == null");

if (arguments.Length < 1)
throw new ArgumentException(Resource.LessParams, nameof(arguments));

Function = arguments[0];
Parameters = arguments[1..];
}

/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(this, obj))
return true;

if (obj is null || GetType() != obj.GetType())
return false;

var other = (Curry)obj;

if (!Function.Equals(other.Function) ||
Parameters.Length != other.Parameters.Length)
return false;

return Parameters.SequenceEqual(other.Parameters);
}

/// <inheritdoc />
public string ToString(IFormatter formatter)
=> formatter.Analyze(this);

/// <inheritdoc />
public override string ToString()
=> ToString(CommonFormatter.Instance);

/// <inheritdoc />
public object Execute()
=> Execute(null);

/// <inheritdoc />
public object Execute(ExpressionParameters? parameters)
{
if (Function.Execute(parameters) is not Lambda lambda)
throw new ResultIsNotSupportedException(this, Function);

var parametersLength = Parameters.Length;

// user provided too many parameters
if (lambda.Parameters.Length < parametersLength)
throw new ArgumentException(Resource.MoreParams);

// user provided exact amount of parameters
if (lambda.Parameters.Length == parametersLength)
return lambda.Call(Parameters, parameters);

lambda = lambda.Curry();

if (parametersLength == 0)
return lambda;

var result = Parameters.Aggregate(
lambda,
(current, parameter) => (Lambda)current.Call(ImmutableArray.Create(parameter), parameters));

return result;
}

/// <inheritdoc />
public TResult Analyze<TResult>(IAnalyzer<TResult> analyzer)
{
if (analyzer is null)
throw new ArgumentNullException(nameof(analyzer));

return analyzer.Analyze(this);
}

/// <inheritdoc />
public TResult Analyze<TResult, TContext>(IAnalyzer<TResult, TContext> analyzer, TContext context)
{
if (analyzer is null)
throw new ArgumentNullException(nameof(analyzer));

return analyzer.Analyze(this, context);

Check warning on line 160 in xFunc.Maths/Expressions/Curry.cs

View check run for this annotation

Codecov / codecov/patch

xFunc.Maths/Expressions/Curry.cs#L160

Added line #L160 was not covered by tests
}

/// <summary>
/// Clones this instance of the <see cref="IExpression" />.
/// </summary>
/// <param name="function">The expression that returns function.</param>
/// <param name="parameters">The list of parameters.</param>
/// <returns>
/// Returns the new instance of <see cref="IExpression" /> that is a clone of this instance.
/// </returns>
public IExpression Clone(IExpression? function = null, ImmutableArray<IExpression>? parameters = null)
=> new Curry(function ?? Function, parameters ?? Parameters);

/// <summary>
/// Gets the expression that returns a lambda to curry.
/// </summary>
public IExpression Function { get; }

/// <summary>
/// Gets the list of parameters.
/// </summary>
public ImmutableArray<IExpression> Parameters { get; }
}
43 changes: 42 additions & 1 deletion xFunc.Maths/Expressions/Lambda.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,30 @@ public override string ToString()
/// </summary>
/// <param name="parameters">An object that contains all parameters and functions for expressions.</param>
/// <returns>A result of the execution.</returns>
public object Call(ExpressionParameters parameters)
public object Call(ExpressionParameters? parameters)
=> Body.Execute(parameters);

/// <summary>
/// Calls the function.
/// </summary>
/// <param name="callParameters">The list of explicit parameters to call lambda with.</param>
/// <param name="expressionParameters">An object that contains all parameters and functions for expressions.</param>
/// <returns>A result of the execution.</returns>
/// <seealso cref="CallExpression"/>
public object Call(ImmutableArray<IExpression> callParameters, ExpressionParameters? expressionParameters)
{
var nestedScope = ExpressionParameters.CreateScoped(CapturedScope ?? expressionParameters);
var zip = Parameters.Zip(callParameters, (parameter, expression) => (parameter, expression));
foreach (var (parameter, expression) in zip)
nestedScope[parameter] = new ParameterValue(expression.Execute(expressionParameters));

var result = Call(nestedScope);
if (result is Lambda lambdaResult)
return lambdaResult.Capture(nestedScope);

return result;
}

/// <summary>
/// Returns a new lambda instance with captured parameters.
/// </summary>
Expand All @@ -99,6 +120,26 @@ public object Call(ExpressionParameters parameters)
public Lambda Capture(ExpressionParameters? parameters)
=> new Lambda(Parameters, Body, parameters);

/// <summary>
/// Converts the current lambda into a list of nested lambdas.
/// </summary>
/// <returns>The lambda.</returns>
/// <seealso cref="Expressions.Curry"/>
public Lambda Curry()
{
if (Parameters.Length <= 1)
return this;

var body = Body;

for (var i = Parameters.Length - 1; i > 0; i--)
body = new LambdaExpression(new Lambda(ImmutableArray.Create(Parameters[i]), body));

var lambda = new Lambda(ImmutableArray.Create(Parameters[0]), body);

return lambda;
}

/// <summary>
/// Converts <see cref="Lambda"/> to <see cref="LambdaExpression"/>.
/// </summary>
Expand Down

0 comments on commit dd04f2e

Please sign in to comment.