patterncsharpMinor
Is this implementation of the Hindley-Milner algorithm correct?
Viewed 0 times
thisthehindleyalgorithmcorrectimplementationmilner
Problem
I based this implementation upon the implementation given in this answer and the book linked in it, but modified it to, instead of a constraint list and a substitution stack, generate a substitution tree in a single tree traversal operation.
Each node in the substitution tree is a
The
The
```
public interface ILambdaTerm { }
public interface IVariableTerm : ILambdaTerm
{
string Name { get; }
}
public interface IAbstractionTerm : ILambdaTerm
{
string ArgumentName { get; }
ILambdaTerm Body { get; }
}
public interface IApplicationTerm : ILambdaTerm
{
ILambdaTerm Function { get; }
ILambdaTerm[] Parameters { get; }
}
public class VariableType
{
public VariableType[] Types;
public VariableType(VariableType argType, VariableType resType)
{
Types = new VariableType[] { argType, resType };
}
public VariableType()
{
Types = new VariableType[0];
}
private bool Contains(VariableType type)
{
if (Types.Length == 0)
return this == type;
else
return Types.Any(subType => subType.Contains(type));
}
public void ReplaceWith(VariableType type)
{
if (type.Contains(this))
throw new Exception("Circular type.");
Types = new VariableType[] { type };
}
public VariableType Deepest()
{
VariableType type = this;
wh
Each node in the substitution tree is a
VariableType. Child nodes are in the Types array. Types with no children represent just variable types, types with one child represent type that have been replaced with the child, and types with two children represent function types. The
Deepest method gets the current substitute given any type, since to replace, say, a with b, provided a is a leaf (has no children), you just set b to be the only child of a. This is what the ReplaceWith method does, which must take a leaf as the type to be replaced. The
Unify method must always get deepest terms (terms with zero or two children).```
public interface ILambdaTerm { }
public interface IVariableTerm : ILambdaTerm
{
string Name { get; }
}
public interface IAbstractionTerm : ILambdaTerm
{
string ArgumentName { get; }
ILambdaTerm Body { get; }
}
public interface IApplicationTerm : ILambdaTerm
{
ILambdaTerm Function { get; }
ILambdaTerm[] Parameters { get; }
}
public class VariableType
{
public VariableType[] Types;
public VariableType(VariableType argType, VariableType resType)
{
Types = new VariableType[] { argType, resType };
}
public VariableType()
{
Types = new VariableType[0];
}
private bool Contains(VariableType type)
{
if (Types.Length == 0)
return this == type;
else
return Types.Any(subType => subType.Contains(type));
}
public void ReplaceWith(VariableType type)
{
if (type.Contains(this))
throw new Exception("Circular type.");
Types = new VariableType[] { type };
}
public VariableType Deepest()
{
VariableType type = this;
wh
Solution
I can't say much about the correctness but I have some comments about the design:
-
Whenever you write code like this:
then this is an indicator that your abstraction is flawed. I'm not saying you should never ever do it but more often than not it indicates a design problem.
-
Another point is the empty
-
Don't throw generic
For the design here is what I would consider changing:
-
Move
-
-
Add a method
Now you can get the
and your
-
Whenever you write code like this:
if (foo is SomeType)
{
// do something
}
else if (foo is SomeOtherType)
{
// do something else
}
...then this is an indicator that your abstraction is flawed. I'm not saying you should never ever do it but more often than not it indicates a design problem.
-
Another point is the empty
ILambdaTerm interface. An interface is like a contract - so what does an empty contract mean? Not much really.-
Don't throw generic
Exception. In your case an InvalidOperationException or maybe your own defined TermValidationException would probably make more sense. This would give the user at least some way of catching specific exceptions.For the design here is what I would consider changing:
-
Move
Unify into VariableType and make it public. Unify only operates on VariableTypes so it makes more sense to live on the VariableType class.-
Types in VariableType can now become private as well which reduces unnecessary exposure of the internal data storage of the class.-
Add a method
VariableType GetVariableType(IScope typeContext) to the ILambdaTerm interface and move the implementation of the logic from the if (term is ...) blocks into the GetVariableType implementation in each of the concrete classes for the terms, e.g.:public interface VariableTerm : IVariableTerm
{
public string Name { get; private set; }
public VariableTerm(string name)
{
Name = name;
}
public override VariableType GetVariableType(IScope typeContext)
{
if (!typeContext.ContainsKey(Name))
throw new Exception(string.Format("Undeclared variable '{0}'.", Name));
return typeContext[Name].Deepest();
}
}Now you can get the
VariableType of a term simply by writing:ILambdaTerm myTerm = ...
...
var variableType = term.GetVariableType(new GlobalScope());and your
TypeChecking class can disappear.Code Snippets
if (foo is SomeType)
{
// do something
}
else if (foo is SomeOtherType)
{
// do something else
}
...public interface VariableTerm : IVariableTerm
{
public string Name { get; private set; }
public VariableTerm(string name)
{
Name = name;
}
public override VariableType GetVariableType(IScope<VariableType> typeContext)
{
if (!typeContext.ContainsKey(Name))
throw new Exception(string.Format("Undeclared variable '{0}'.", Name));
return typeContext[Name].Deepest();
}
}ILambdaTerm myTerm = ...
...
var variableType = term.GetVariableType(new GlobalScope<VariableType>());Context
StackExchange Code Review Q#38763, answer score: 2
Revisions (0)
No revisions yet.