这个 Java Kata 的更好的函数式解决方案

Posted

技术标签:

【中文标题】这个 Java Kata 的更好的函数式解决方案【英文标题】:A better Functional solutions to this Java Kata 【发布时间】:2021-12-26 22:45:36 【问题描述】:

我正在尝试了解有关函数式编程的更多信息。我做了一些训练视频,我想我会做一个 Kata。

也许我只是选了一个不好的,我试图用谓词来做过滤,但它似乎添加了比看起来需要的更多的代码。我相信有更好的方法。

谢谢!

卡塔 --------

n:         2
oldValue: 'a'
newValue: 'o'
"Vader said: No, I am your father!" -> "Vader soid: No, I am your fother!"
  1     2          3        4       -> 2nd and 4th occurence are replaced

    package kata;
    
    import java.util.ArrayList;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.function.BiPredicate;
    
    public class ReplaceEveryNth 
        public static Integer interPos = 0;
        
        //works but isn't great. How do you make this functional?

        public static String replaceNth(String text, Integer n, Character oldValue, Character newValue)
            //String builder allows for the setCharAt Function to swap char
            StringBuilder sb = new StringBuilder(text);
    
            //make an array of all the positions the char is found at.
            ArrayList<Integer> foundAt = new ArrayList<>();
            sb.toString().chars().forEach( c -> 
                interPos++;
                if (c == oldValue) foundAt.add(interPos);
            );
    
            //need to for mod div
            AtomicInteger index = new AtomicInteger();
            index.set(1);
    
            //if mod this pos is 0, then swap it.
            foundAt.forEach(pos -> 
                System.out.println("pos Mods: " + pos + " " + index);
                if (index.get() % n == 0) 
                    sb.setCharAt(pos-1, newValue);
                
                index.getAndIncrement();
            );
    
            return sb.toString();
        
    
    

编辑这个更新的方法,它不是真的有用吗?但它使用单个循环。

    public static String replaceNth(String text, Integer n, Character oldValue, Character newValue)
        char[] chars = text.toCharArray();
        char[] ret = new char[chars.length];
        int counter = 1;

        for (int i = 0; i < chars.length; ++i)
        
            ret[i] = chars[i];
            if (chars[i] == oldValue) 
                    if (counter % n == 0) 
                        ret[i] = newValue;
                        counter = 0;
                    
                counter++;
            
        
        return new String(ret);
    

【问题讨论】:

为什么要保留索引?您可以将它们替换为找到的,这样您只需要一个流。 没有其他原因......原来我做了2个谓词...... static BiPredicate shouldReplace = (pos, lastFound) -> if ((lastFound + pos) = = interPos) System.out.println("FOUND"); 返回真; ;静态 BiPredicate foundReplaceChar = (c, oldValue) -> c.equals(oldValue);并尝试使用一个循环,但它并没有使任何东西更容易阅读或不那么复杂。但是,我可能只是做的不好。 我曾希望尝试做出类似的东西:return Stream.doSomething(predicate).dosomethingelse(whatever); 你不能用谓词过滤你的字符流,因为你甚至需要不等于旧值的字符才能在结果字符串中结束。要替换字符,您可以.map() 每个字符都使用一元运算,这是某种“基于函数”的解决方案。 你接近它的方式不是很实用。一个迹象是您正在使用可变数据 (StringBuilder)。一种“更实用”的方法是 1. 按 oldValue ("a") 拆分字符串,2. 创建一系列分隔符('a'、'o'、'a'、'o' 等),最后 3. 然后将这些片段再次连接在一起,将分割值与序列交替。上面的每一步都可以是它自己的函数(可能是已经给定的,比如 String.split) 【参考方案1】:

不确定是否可以在此处使用正则表达式方法,但通常可以创建参数化模式以将 oldValue 的每 N 次出现替换为 newValue

"(oldValue)([^oldValue]*(oldValue[^oldValue]*)occurrence - 2)(oldValueToReplace)" 这里使用了 3 个组:

    (oldValue) - 第一次出现 ([^oldValue]*(oldValue[^oldValue]*)occurrence - 2) - 零个或多个非oldValue 条目,后跟oldValue,后缀应出现occurrence - 2(oldValueToReplace) - 第 N 次被替换。

另外,字符类[]之外的值应该被转义。

示例实现:

public static String replaceNth(String text, int n, char oldValue, char newValue) 
    String pattern = String.format("(%2$s)([^%1$s]*(%2$s[^%1$s]*)%3$d)(%2$s)", oldValue, Pattern.quote(String.valueOf(oldValue)), n - 2);
    
    return text.replaceAll(pattern, "$1$2" + newValue);
    

测试:

System.out.println(replaceNth("Vader said: No, I am your father!", 2, 'a', 'o'));

System.out.println(replaceNth("... .... ..... .... ... ", 3, '.', 'x'));

System.out.println(replaceNth("xxxx abcd x dbca xxxx", 5, 'x', 'O'));

System.out.println(replaceNth("+---------------------", 7, '-', '+'));

输出:

Vader soid: No, I am your fother!
..x ..x. .x..x ..x. .x. 
xxxx abcd O dbca xxxx
+------+------+------+

【讨论】:

感谢您添加此内容,这是一个很好的答案,在工作场所我将如何处理它。【参考方案2】:

Dave,我整理了一些我认为更实用的东西(我不是函数式编程专家,我的 Java 有点生疏)。我认为我在“玩得开心”而不是更直接地回答你的问题上走得太远了,所以我很抱歉。

根据我的阅读,函数式编程的关键之一是识别“纯函数”(没有副作用的函数)并以此为基础进行构建。几乎所有有用的代码也会有不纯的函数,但是通过将尽可能多的逻辑转移到纯函数中有助于将不纯的东西集中在希望被很好地包含的区域。

我所做的是将问题分解为几个不同的通用功能,并使用它们来解决手头的问题。我没有马上得到这个解决方案。我开始分解事物,随着工作的进行,我将功能调整为越来越通用。我花了几次迭代才得到您在下面看到的内容。早期的迭代有点难看,但当我完成工作时,我很惊喜地发现我能够达到现在的水平。

感谢您提出这个问题。我学到了很多试图回答它。我希望反过来我也能帮助你。

// Splits a string by the given character into a list of sub-strings
public static List<String> splitBy(String text, Character splitValue) 
    return Arrays.stream(text.split(Pattern.quote(splitValue.toString()), -1)).toList();


// Generates an infinite sequence where every Nth item is one value
// and all other values are another
// generateEveryNthSequence(3, 'A', 'B') =? ['B', 'B', 'A', 'B', 'B', 'A', ...]
public static <T> Stream<T> generateEveryNthSequence(int n, T everyNthValue, T everyOtherValue) 
    return Stream.iterate(1, i -> i + 1).map(i -> i % n == 0 ? everyNthValue : everyOtherValue);


// Combines two sequences by alternating the values
// ['A','B','C'] and ['1','2','3'] => ['A', '1', 'B, '2', 'C', '3']
public static <T> Stream<T> alternateValues(Stream<T> stream1, Stream<T> stream2) 
    Iterator<T> iterator1 = stream1.iterator();
    Iterator<T> iterator2 = stream2.iterator();
    return Stream.iterate(iterator1, t -> t == iterator1 ? iterator2 : iterator1)
            .takeWhile(t -> t.hasNext())
            .map(t -> t.next());


public static String replaceNth(String text, Integer n, Character oldValue, Character newValue)
    // "V", "der s", "id: No, I ", "m your f", "ther!"
    List<String> segments = splitBy(text, oldValue);
    // "a", "o", "a", "o", ...
    Stream<String> separators = generateEveryNthSequence(n, newValue.toString(), oldValue.toString());
    // "V", "a", "der s", "o", "id: No, I ", "a", "m your f", "o", "ther!", "a"
    Stream<String> alternatingItems = alternateValues(segments.stream(), separators);
    // "V", "a", "der s", "o", "id: No, I ", "a", "m your f", "o", "ther!"
    Stream<String> alternatingItemsTrimmed = alternatingItems.limit(segments.size() * 2 - 1);
    // "Vader soid: No, I am your fother!"
    return alternatingItemsTrimmed.collect(Collectors.joining());

【讨论】:

非常感谢您浏览此内容,我希望我能看到您的流程。我了解它所训练的概念,在实践中确定它们是我的目标。我将对此进行反工程并进行研究。非常感谢您的时间和帮助。 public static Stream generateEveryNthSequence(int n, T everyNthValue, T everyOtherValue) 让我印象深刻的部分是这个。 1. 我不知道如何在steam中访问索引,这很好学!为什么我在尝试遵循您的建议时做了一个 for 循环。 2. Stream.iterate(1, i -> i + 1)。创建一个索引为 1 的新空流,为每一步添加 1。直到....?它可以工作,但如果我把它写到屏幕上,我会等待板子。会持续到 2,147,483,647 吗?【参考方案3】:

我想提出一个替代解决方案

public class App 

public static void main(String[] args) 
    String input = "Vader said: No, I am your father!";
    String result = replacenth(input, 'a', 'o', 2);
    System.out.println(input);
    System.out.println(result);
    System.out.println(result.equalsIgnoreCase("Vader soid: No, I am your fother!"));


private static String replacenth(String input, char search, char replace, int n) 
    return IntStream.range(1, input.length() + 1)
            .mapToObj(i -> input.substring(0, i))
            .map(s -> shouldReplace(s, search, n)
                    ? replace : s.charAt(s.length() - 1))
            .collect(Collector.of(
                    StringBuilder::new,
                    StringBuilder::append,
                    StringBuilder::append,
                    StringBuilder::toString));


private static boolean isEqual(String s, char c) 
    return s.charAt(s.length() -1) == c;

private static Long countOccurences(String s, char c) 
    return s.chars().filter(x -> x == c).count();

private static boolean shouldReplace(String s, char search, int n) 
    return isEqual(s, search) && countOccurences(s, search) % n == 0;

【讨论】:

以上是关于这个 Java Kata 的更好的函数式解决方案的主要内容,如果未能解决你的问题,请参考以下文章

在 Java 中提升函数以更好地“函数式”编程

Spring5-Reactor函数式编程

Spring5-Reactor函数式编程

函数式编程学习资料汇总

同一个问题有多个解

[一] java8 函数式编程入门 什么是函数式编程 函数接口概念 流和收集器基本概念