TypeName cataMapperTypeName(DataConstructor dc) {

    TypeName[] argsTypeNames =
        concat(
                dc.arguments().stream().map(DataArgument::type),
                dc.typeRestrictions()
                    .stream()
                    .map(TypeRestriction::idFunction)
                    .map(DataArgument::type))
            .map(t -> Utils.asBoxedType.visit(t, utils.types()))
            .map(this::substituteTypeWithRecursionVar)
            .map(TypeName::get)
            .toArray(TypeName[]::new);

    return adt.dataConstruction().isVisitorDispatch()
        ? argsTypeNames.length == 0
            ? ParameterizedTypeName.get(
                ClassName.get(FlavourImpl.findF0(context.flavour(), utils.elements())),
                TypeName.get(adt.matchMethod().returnTypeVariable()))
            : argsTypeNames.length == 1
                ? ParameterizedTypeName.get(
                    ClassName.get(FlavourImpl.findF(context.flavour(), utils.elements())),
                    argsTypeNames[0],
                    TypeName.get(adt.matchMethod().returnTypeVariable()))
                : ParameterizedTypeName.get(
                    Utils.getClassName(context, MapperDerivator.mapperInterfaceName(dc)),
                    concat(
                            concat(
                                dc.typeVariables().stream().map(TypeVariableName::get),
                                fold(
                                    MapperDerivator.findInductiveArgument(utils, adt, dc),
                                    Stream.<TypeName>of(),
                                    tm ->
                                        Stream.of(
                                            ParameterizedTypeName.get(
                                                ClassName.get(
                                                    FlavourImpl.findF0(
                                                        context.flavour(), utils.elements())),
                                                TypeName.get(
                                                    adt.matchMethod().returnTypeVariable()))))),
                            Stream.of(TypeVariableName.get(adt.matchMethod().returnTypeVariable())))
                        .toArray(TypeName[]::new))
        : TypeName.get(
            utils
                .types()
                .getDeclaredType(
                    Utils.asTypeElement.visit(dc.deconstructor().visitorType().asElement()).get(),
                    dc.deconstructor()
                        .visitorType()
                        .getTypeArguments()
                        .stream()
                        .map(this::substituteTypeWithRecursionVar)
                        .toArray(TypeMirror[]::new)));
  }
 TypeMirror substituteTypeWithRecursionVar(TypeMirror tm) {
   return utils.types().isSameType(tm, adt.typeConstructor().declaredType())
       ? utils
           .types()
           .getDeclaredType(
               FlavourImpl.findF0(context.flavour(), utils.elements()),
               adt.matchMethod().returnTypeVariable())
       : tm;
 }
  private DeriveResult<DerivedCodeSpec> functionDispatchImpl(List<DataConstructor> constructors) {

    NameAllocator nameAllocator = nameAllocator(constructors);

    TypeElement f = FlavourImpl.findF(context.flavour(), utils.elements());
    TypeName returnType =
        TypeName.get(
            utils
                .types()
                .getDeclaredType(
                    f,
                    adt.typeConstructor().declaredType(),
                    adt.matchMethod().returnTypeVariable()));

    TypeSpec wrapper =
        TypeSpec.anonymousClassBuilder("")
            .addField(
                FieldSpec.builder(returnType, nameAllocator.get("cata"))
                    .initializer(
                        CodeBlock.builder()
                            .addStatement(
                                "$L -> $L.$L($L)",
                                nameAllocator.get("adt var"),
                                nameAllocator.get("adt var"),
                                adt.matchMethod().element().getSimpleName(),
                                Utils.joinStringsAsArguments(
                                    constructors
                                        .stream()
                                        .map(
                                            constructor ->
                                                constructor
                                                        .arguments()
                                                        .stream()
                                                        .map(DataArguments::getType)
                                                        .noneMatch(
                                                            tm ->
                                                                utils
                                                                    .types()
                                                                    .isSameType(
                                                                        tm,
                                                                        adt.typeConstructor()
                                                                            .declaredType()))
                                                    ? constructor.name()
                                                    : CodeBlock.builder()
                                                        .add(
                                                            "($L) -> $L.$L($L)",
                                                            Utils.asLambdaParametersString(
                                                                constructor.arguments(),
                                                                constructor.typeRestrictions()),
                                                            constructor.name(),
                                                            MapperDerivator.mapperApplyMethod(
                                                                utils, context, constructor),
                                                            Utils.joinStringsAsArguments(
                                                                Stream.concat(
                                                                    constructor
                                                                        .arguments()
                                                                        .stream()
                                                                        .map(
                                                                            argument ->
                                                                                utils
                                                                                        .types()
                                                                                        .isSameType(
                                                                                            argument
                                                                                                .type(),
                                                                                            adt.typeConstructor()
                                                                                                .declaredType())
                                                                                    ? "() -> this."
                                                                                        + nameAllocator
                                                                                            .get(
                                                                                                "cata")
                                                                                        + "."
                                                                                        + FlavourImpl
                                                                                            .functionApplyMethod(
                                                                                                utils,
                                                                                                context)
                                                                                        + "("
                                                                                        + argument
                                                                                            .fieldName()
                                                                                        + ")"
                                                                                    : argument
                                                                                        .fieldName()),
                                                                    constructor
                                                                        .typeRestrictions()
                                                                        .stream()
                                                                        .map(
                                                                            TypeRestriction
                                                                                ::idFunction)
                                                                        .map(
                                                                            DataArgument
                                                                                ::fieldName))))
                                                        .build()
                                                        .toString())))
                            .build())
                    .build())
            .build();

    MethodSpec cataMethod =
        MethodSpec.methodBuilder("cata")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
            .addTypeVariables(
                Stream.concat(
                        adt.typeConstructor().typeVariables().stream(),
                        Stream.of(adt.matchMethod().returnTypeVariable()))
                    .map(TypeVariableName::get)
                    .collect(Collectors.toList()))
            .returns(returnType)
            .addParameters(
                constructors
                    .stream()
                    .map(
                        dc ->
                            ParameterSpec.builder(
                                    cataMapperTypeName(dc), MapperDerivator.mapperFieldName(dc))
                                .build())
                    .collect(toList()))
            .addStatement("return $L.$L", wrapper, nameAllocator.get("cata"))
            .build();

    return result(methodSpec(cataMethod));
  }
  public static DeriveResult<DerivedCodeSpec> derive(
      AlgebraicDataType adt, DeriveContext deriveContext, DeriveUtils deriveUtils) {

    // skip constructors for enums
    if (adt.typeConstructor().declaredType().asElement().getKind() == ElementKind.ENUM) {
      return result(none());
    }

    TypeConstructor typeConstructor = adt.typeConstructor();
    TypeElement lazyTypeElement =
        FlavourImpl.findF0(deriveContext.flavour(), deriveUtils.elements());
    TypeName lazyArgTypeName =
        TypeName.get(
            deriveUtils.types().getDeclaredType(lazyTypeElement, typeConstructor.declaredType()));
    String lazyArgName = Utils.uncapitalize(typeConstructor.typeElement().getSimpleName());
    TypeName typeName = TypeName.get(typeConstructor.declaredType());

    List<TypeVariableName> typeVariableNames =
        adt.typeConstructor()
            .typeVariables()
            .stream()
            .map(TypeVariableName::get)
            .collect(Collectors.toList());

    String className = "Lazy";
    TypeSpec.Builder typeSpecBuilder =
        TypeSpec.classBuilder(className)
            .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
            .addTypeVariables(typeVariableNames)
            .addField(
                FieldSpec.builder(
                        TypeName.get(Object.class), "lock", Modifier.PRIVATE, Modifier.FINAL)
                    .initializer("new Object()")
                    .build())
            .addField(FieldSpec.builder(lazyArgTypeName, "expression", Modifier.PRIVATE).build())
            .addField(
                FieldSpec.builder(typeName, "evaluation", Modifier.PRIVATE, Modifier.VOLATILE)
                    .build())
            .addMethod(
                MethodSpec.constructorBuilder()
                    .addParameter(ParameterSpec.builder(lazyArgTypeName, lazyArgName).build())
                    .addStatement("this.expression = $N", lazyArgName)
                    .build())
            .addMethod(
                MethodSpec.methodBuilder("eval")
                    .addModifiers(Modifier.PRIVATE)
                    .returns(typeName)
                    .addCode(
                        CodeBlock.builder()
                            .addStatement("$T _evaluation = this.evaluation", typeName)
                            .beginControlFlow("if (_evaluation == null)")
                            .beginControlFlow("synchronized (this.lock)")
                            .addStatement("_evaluation = this.evaluation")
                            .beginControlFlow("if (_evaluation == null)")
                            .addStatement(
                                "this.evaluation = _evaluation = expression.$L()",
                                Utils.getAbstractMethods(lazyTypeElement.getEnclosedElements())
                                    .get(0)
                                    .getSimpleName())
                            .addStatement("this.expression = null")
                            .endControlFlow()
                            .endControlFlow()
                            .endControlFlow()
                            .addStatement("return _evaluation")
                            .build())
                    .build())
            .addMethod(
                Utils.overrideMethodBuilder(adt.matchMethod().element())
                    .addStatement(
                        "return this.eval().$L($L)",
                        adt.matchMethod().element().getSimpleName(),
                        Utils.asArgumentsStringOld(adt.matchMethod().element().getParameters()))
                    .build());

    if (adt.typeConstructor().declaredType().asElement().getKind() == ElementKind.INTERFACE) {
      typeSpecBuilder.addSuperinterface(typeName);
    } else {
      typeSpecBuilder.superclass(typeName);
    }

    typeSpecBuilder.addMethods(
        optionalAsStream(
                findAbstractEquals(typeConstructor.typeElement(), deriveUtils.elements())
                    .map(
                        equals ->
                            deriveUtils
                                .overrideMethodBuilder(equals)
                                .addStatement(
                                    "return this.eval().equals($L)",
                                    equals.getParameters().get(0).getSimpleName())
                                .build()))
            .collect(Collectors.toList()));

    typeSpecBuilder.addMethods(
        optionalAsStream(
                findAbstractHashCode(typeConstructor.typeElement(), deriveUtils.elements())
                    .map(
                        hashCode ->
                            deriveUtils
                                .overrideMethodBuilder(hashCode)
                                .addStatement("return this.eval().hashCode()")
                                .build()))
            .collect(Collectors.toList()));

    typeSpecBuilder.addMethods(
        optionalAsStream(
                findAbstractToString(typeConstructor.typeElement(), deriveUtils.elements())
                    .map(
                        toString ->
                            deriveUtils
                                .overrideMethodBuilder(toString)
                                .addStatement("return this.eval().toString()")
                                .build()))
            .collect(Collectors.toList()));

    return result(
        codeSpec(
            typeSpecBuilder.build(),
            MethodSpec.methodBuilder("lazy")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addTypeVariables(
                    typeConstructor
                        .typeVariables()
                        .stream()
                        .map(TypeVariableName::get)
                        .collect(Collectors.toList()))
                .addParameter(lazyArgTypeName, lazyArgName)
                .returns(typeName)
                .addStatement(
                    "return new $L$L($L)",
                    className,
                    typeVariableNames.isEmpty() ? "" : "<>",
                    lazyArgName)
                .build()));
  }